【Redis】Redis中的五种数据结构及位图、HyperLogLog、GEO等
Redis
redis API
通用命令
keys
计算数据库所有的键 时间复杂度O(n)【一般在生产环境中使用】
> keys * #遍历所有key
>
> ```mysql
> 127.0.0.1:6379 > set hello word
> 127.0.0.1:6379 > set java good
> 127.0.0.1:6379 > set php best
> 127.0.0.1:6379 > keys *
> 1)"java"
> 2)"hello"
> 3)"php"
> 127.0.0.1:6379 > dbsize
> <Integer> 3
> ```
>
> keys [pattern] #遍历所有key
>
> ```mysql
> 127.0.0.1:6379 > mset hello world hehe haha php good phe his
> 127.0.0.1:6379 > keys he*
> 1)"hehe"
> 2)"hello"
> 127.0.0.1:6379 > keys he[h-l]*
> 1)"hehe"
> 2)"hello"
> 127.0.0.1:6379 > keys ph?
> 1)"phe"
> 2)"php"
> ```
>
>
应用:1. 热备从节点 2. scan
dbsize
算出数据库的大小,10个key,大小就是10
dbsize #计算key的总数
127.0.0.1:6379 > mset k1 v1 k2 v2 k3 v3 k4 v4 127.0.0.1:6379 > dbsize (integer) 4 127.0.0.1:6379 > sadd myset a b c d e (integer) 5 127.0.0.1:6379 > dbsize (integer) 5
exists key
判断key是否存在
exits key #检查key是否存在 ——存在则返回1,不存在则返回0
127.0.0.1:6379 > set a b 127.0.0.1:6379 > exists a (integer) 1 127.0.0.1:6379 > del a (integer) 1 127.0.0.1:6379 > exists a (integer) 0
del key [key …]**
删除key,可以删除多个key
del key #删除指定key-value ——删除成功则返回1,key不存在则返回0
127.0.0.1:6379 > set a b 127.0.0.1:6379 > get a "b" 127.0.0.1:6379 > del a (integer) 1 127.0.0.1:6379 > get a (nil)
expire key seconds
设置key的过期时间,在一段时间内自动删除
expire key seconds #key在seconds秒后过期
ttl key # 查看key剩余的过期时间
persist key # 去掉 key的过期时间
127.0.0.1:6379 > set hello world 127.0.0.1:6379 > expire hello 20 (integer) 1 127.0.0.1:6379 > ttl hello (integer) 16 127.0.0.1:6379 > get hello "world" 127.0.0.1:6379 > ttl hello (integer) 7 127.0.0.1:6379 > ttl hello (integer) -2 (-2代表key已经不存在了) 127.0.0.1:6379 > get hello (nil)
127.0.0.1:6379 > set hello world 127.0.0.1:6379 > expire hello 20 (integer) 1 127.0.0.1:6379 > ttl hello (integer) 16(还有16秒过期) 127.0.0.1:6379 > persist hello (integer) 1 127.0.0.1:6379 > ttl hello (integer) -1 (-1代表key存在,并没有过期时间) 127.0.0.1:6379 > get hello "world"
type key
查看key的数据类型
type key # 返回key的类型——string、hash、list、set、zset、none
127.0.0.1:6379 > set a b 127.0.0.1:6379 > type a string 127.0.0.1:6379 > sadd myset 1 2 3 (integer) 3 127.0.0.1:6379 > type myset set
命令 | 时间复杂度 |
---|---|
keys | O(n) |
dbsize | O(1) |
del | O(1) |
exists | O(1) |
expire | O(1) |
type | O(1) |
数据结构和内部编码
单线程架构
redis在一瞬间只会执行一条命令
-
为什么redis使用的是单线程还这么快呢?
-
纯内存
内存的响应速度是很快的,比硬盘运行速度快很多
-
非阻塞IO
将连接,读写,关闭转换为自身的一个事件,不在IO上浪费过多的时间
-
避免线程切换和竞争状态的消耗
多线程需要线程切换,但是单线程不需要涉及线程切换和竞争状态,避免了消耗
-
redis 服务器启动时,会把 AE_READABLE 向 eventLoop(IO 多路复用)注册。
-
客户端请求与服务器建立连接,服务器生成 Scoket(s1)通道并绑定 AE_READABLE 事件。
-
客户端(s1)请求执行 set key value(写命令),s1 触发 AE_READABLE 由命令请求器将 key 和 value 读取到内存并对数据修改
-
设值结束后将 s1 与 AE_WRITABLE 事件关联绑定,从而触发写操作,成功后由命令回复器输出结果“OK”
-
返回结果将 s1 的 AE_WRITABLE 事件与命令回复处理器解除绑定。
-
-
使用单线程要注意什么?
-
一次只运行一条命令
-
拒绝长(慢)命令
keys,flushall,flushdb,slow lua script,mutil/exec,operate big value(collection)
-
其实不是单线程
fysnc file descriptor
close file descriptor
-
redis 数据结构
string
redis所有的key都是一个字符串,字符串的上限是512M,建议几百K
命令:
setnx 分布式锁
- get set del # 获取键 设置键 删除键
incr key # key自增1,如果key不存在,自增后get(key) = 1
decr key # key自减1,如果key不存在,自减后get(key)= -1
incrby key k # key自增k,如果key不存在,自增后get(key) = k
decrby key k # key自减k,如果key不存在,自减后get(key) = -k
- set setnx setxx
## ex seconds:为键设置秒级过期时间。px milliseconds:为键设置毫秒级过期时间。 ## nx:键必须不存在,才可以设置成功,用于添加。xx:与nx相反,键必须存在,才可以设置成功,用于更新。
et key value [ex seconds] [px milliseconds] [nx|xx]
set key value # 不管key是否存在,都设置
setnx key value # key不存在,才设置
set key value xx # key存在,才设置
mget mset
n次get = n次网络时间 + n次命令时间
1次mget = 1次网络时间 + n次命令时间
getset append strlen
incrbyfloat getrange setrange
decr(自减)、incrby(自增指定数字)、 decrby(自减指定数字)、incrbyfloat(自增浮点数)
应用:
-
缓存
-
计数器
-
分布式锁
举例:
-
记录网站每个用户个人主页的访问量?
incr key value
| | |
incr userid:pageview count (单线程:无竞争)
因为是天生单线程的,在并发执行incr的时候,不会有竞争问题,在执行计数的时候每个key的自增都是独立去执行的,不会记错数
-
缓存视频的基本信息(数据源在MySQL)中,伪代码
解释:很多网站的基本信息存储在mysql中,但是为了提高接口的访问性能或者说是并发量,会把视频的基本信息存在redis中
实现思路:
-
通过视频的vid,获取视频的信息
-
从redis来获取
-
如果redis中存在,说明redis缓存中有这样的信息,不需要从后端的数据库中查,减小后端数据库的访问压力,则返回给app server;如果不存在,则通过redis从服务器中查找数据
-
查找到数据,将数据写入到redis中,方便下次查找
-
将查找的数据返回给app server
伪代码实现:
-
public VedioInfo get(long id){
String redisKey = redisPrefix + id; //可能是id + vedio的前缀
VideoInfo videoInfo = redis.get(redisKey);
if(videoInfo == null){
videoInfo = mydql.get(id);//从数据库中取
if(videoInfo != null){ //如果数据库中取到的值不是空
//序列化
redis.set(redisKey,serialize(videoInfo)); //序列化
}
}
return videoInfo;
}
-
实现分布式id生成器
incr id
hash
特点:map-map、small redis、field不能相同,value可以相同
命令:
hget key field # 获取hash key对应的field的value
hsetkey field # 设置hash key对应的field的value
hdel key field # 删除hash key对应的field的value
hexists key field # 判断hash key是否有field
hlen key # 获取hash key field的数量
hmget hmset
举例:
-
记录网站每个用户个人主页的访问量?
hincrby key field value
hincrby user:1:info pageview count ——相当于给key:user:1:info添加了一个属性,pageview count进行每个用户个人主页的访问量
-
缓存视频的基本信息(数据源在MySQL)中,伪代码
public VedioInfo get(long id){
String redisKey = redisPrefix + id; //可能是id + vedio的前缀
Map<String,String> hashMap = redis.hgetAll(redisKey);
VideoInfo videoInfo = transferMapToVideo(hashMap);
if(videoInfo == null){
videoInfo = mydql.get(id);//从数据库中取
if(videoInfo != null){ //如果数据库中取到的值不是空
redis.hmset(redisKey,transferMapToVideo(videoInfo));
}
}
return videoInfo;
}
list
key elements
左边是key,右边是value(这里的value是一个有序的队列)
特点:有序、可以重复、左右两边插入弹出
一个列表最多可以存储 2 的 32 次方减 1 个元素,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素
命令:
插入:rpush、lpushlinsert
删除:lpop、rpop、lrem、ltrim
查:lrange、lindex、llen
改:lset、
blpop brpop
lpush + lpop = stack
lpush + rpop = queue
lpush + ltrim = capped collection
lpush + brpop = message queue
举例:
微博的TimeLine
关注用户的时间轴来排序,一定范围内来按照每十页进行分页lrange
你关注的人更新微博,lpush,把更新的微博放到队头来显示
最新列表,List 类型的 lpush 命令和 lrange 命令能实现最新列表的功能,每次通过 lpush 命令往列表里插入新的元素,然后通过 lrange 命令读取最新的元素列表,如朋友圈的点赞列表、评论列表。
排行榜, List 类型的 lrange 命令可以分页查看队列中的数据, 但是只有定时计算的排行榜才适合使用 list 类型存储(实时不行)。
set
集合:集合不允许添加重复元素——无序、无重复、集合间操作
sinter:取出两个集合中相同的元素
sdiff:取出与另一个集合不同的元素
sunion:把两个集合的∪并集收集起来
sdiff | sinter | suion + store destkey …将差集、交集、并集结果保存在destkey中
sadd 添加元素
srem 删除元素
scard 算集合中元素的个数
sismember 元素是否在集合中
srandmember 从集合中随机取出一个元素——不破坏集合的元素
smembers 取出集合中所有的元素
spop 从集合中随机弹出一个元素——弹出之后就没有了
举例:
抽奖系统:用spop进行弹出,因为用户量非常大,如果用srandmember会阻塞队列
like、赞、踩:存在集合中
标签(tag):(下面两点是同个事务)
用集合给用户添加标签
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
共同关注:共同关注的好友、共同关注的兴趣都可以用集合来实现
黑白名单,有业务出于安全性方面的考虑,需要设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中。
TIPS:SADD 命令做一些标签的使用;spop / srandmember 命令做一些随机数的使用;sadd + sinter 命令做一些社会关系图,如共同关注的好友
zset
有序集合,其实"值”中存储的是一个集合,但是集合中每个元素是有排序的,score - value 通过分数(优先级)进行排序
集合 VS 有序集合
集合 | 有序集合 |
---|---|
无重复元素 | 无重复元素 |
无序 | 有序 |
element | element + score |
有序集合 VS 列表
有序集合 | 列表 |
---|---|
无重复元素 | 可以有重复元素 |
有序 | 有序 |
element + score | element |
命令:
zadd 添加操作 # zadd key score element (分数可以重复,但是element不可以重复,即班级人员的不可重复,但是考试成绩的分数可以重复)
zrem 删除元素 zrem key element
zscore 返回元素的分数 zscore key element
zincrby 增加或减少元素的分数 zincrby key increScore element # zincrby user:1:ranking 9 mike(给mike的分数增加9分)
zcard 返回集合中元素的个数
zrank 获取某个元素的排名,从小到大的排名
zrange 返回指定索引范围内的升序元素[分值] zrange key start end [分值] O(log(n) + m)
zrangebyscore 返回按照分数范围内的升序元素
zcount 返回有序集合内在指定分数范围内的个数
zremrangebyrank 删除指定排名内的升序元素
zremrangebyscore 删除指定分数内的升序元素
举例:
标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。
共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。
统计网站的独立 IP。利用 set 集合当中元素不唯一性,可以快速实时统计访问网站的独立 IP。
统计用户的点赞/取消点赞
排行榜功能,比如展示获取赞数最多的十个用户
操作类型 | 命令 |
---|---|
基本操作 | zadd、zrem、zcard、zincrby、zscore |
范围操作 | zrange、zrangebyscore、zcount、zremrangebyrank |
集合操作 | zunionstore、zinterstore |
位图
对redis来说,可以直接去操作数据的位
set hello big
setbit key offset value # 给位图指定索引设置值
gitbit key offset # 获取位图指定索引的值 getbit hello 1 (获取hello键中的值big的位图的第二位)
bitcount key [start end] #获取位图指定范围(start到end,单位为字节,如果不指定就是获取全部)位值为1的个数
bitop op destkey key [key…] #做多个bitmap的and(交集)、or(并集)、not(非)、xor(异或),操作并将结果保存在destkey中
bitpos key targetBit [start] [end] #计算位图指定范围
举例:
-
独立用户统计:
-
使用set和bitmap进行对比,当一个网站有1亿用户,并且有5千万用户每天在独立访问
当用set进行操作是,需要存储的用户量是50,000,000,因为userid可能是长整型的,则占32位,则全部内存量为32位 * 50,000,000 = 200MB
而,bitmap,位图每个userid占用1位,当需要存储的的用户量是100,000,000时,需要的全部内存量为1位 * 100,000,000 = 12.5MB
占用内存对比:
一天 一个月 一年 set 200M 6G 72G bitmap 12.5M 375M 4.5G -
只有十万独立用户呢
数据类型 每个userid占用空间 需要存储的用户量 全部内存量 set 32位(假设userid用的是整型,很多网站用的是长整型) 1,000,000 32 * 1,000,000 = 4MB bitmap 1位 100,000,000 1 *100,000,000 = 12.5M
-
HyperLogLog
HyperLogLog算法,是利用极小空间完成独立数量统计。本质结构还是字符串
三个命令:
-
pfadd key element [element] :向HyperLogLog添加元素
-
pfcount key [key …] 计算HyperLogLog的独立总数
-
pfmerge destkey sourcekey [sourcekey…] 合并多个HyperLogLog
内存消耗(百万独立用户)???
GEO
地理信息定位:存储经纬度,计算两地距离,范围计算
redis一些知识
Jedis
生成一个Jedis对象,这个对象负责和指定redis节点通信
Jedis jedis = new Jedis(“127.0.0.1”,6379);
jedis执行set操作
jedis.set(“hello”,“world”);
jedis执行get操作,value = “world”
String value = jedis.get(“hello”)
Jedis(String host,int port,int connectionTimeout,int soTimeout)
-
host :redis节点的所在的机器的IP
-
port :redis节点的端口
-
connectionTimeout:客户端连接超时
-
soTimeout:客户端读写超时
jedis直连:
jedis连接池:
优点 | 缺点 | |
---|---|---|
直连 | - 简单方便 适用于少量长期连接的场景 | - 存在每次新建/关闭TCP开销 资源无法控制,存在连接泄露的可能 Jedis对象线程不安全 |
连接池 | Jedis预先生成,降低开销使用 连接池的形式保护和控制资源的使用 | 相对于直连,使用相对麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题 |
慢查询
-
生命周期
- 慢查询一般发生在第3阶段
- 客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素
-
两个配置
slowlog-max-len:
1. 先进先出队列
2. 固定长度
3. 保存在内存内
slowlog-log-slower-than:
1. 慢查询阈值(单位:微秒) 2. slow-log-slower-than=0,记录所有命令 3. slowlog-log-slower-than<0,不记录任何命令
配置方法:
-
默认值
config get slowlog-max-len = 128
config get slowlog-log-slower-than = 10000
-
修改配置文件重启
-
动态配置
config set slowlog-max-len 1000
config set slowlog-log-slower-than 1000
-
-
三个命令
慢查询命令
slowlog get [n] :获取慢查询队列
slowlog len:获取慢查询队列长度
slowlog reset :清空慢查询队列
-
运维经验
1. slowlog-max-len 不要设置过大,默认10ms,通常设置1ms 2. slowlog-log-slower-than 不要设置过小,通常设置1000左右 3. 理解命令生命周期 4. 定期持久化慢查询
pipeline
它能将一组 Redis 命令进行组装,通过一次传输给 Redis 并返回结果集。
-
什么流水线
节省网络连接 时间的开销
1 次pipeline(n条命令) = 1 次网络时间 + n 次命令时间
命令 n个命令操作 1次pipeline(n个命令) 时间 n次网络+n次命令 1次网络+n次命令 数据量 1条命令 n条命令 注意:
-
redis的命令时间是微秒级别
-
pipeline每次条数要控制(网络)
流水线的作用:
当我们需要从北京发往上海的数据,光纤的速度是光速的2/3,则一次命令传输时间则大约是13毫秒,但是redis操作的时间可能是几微秒,但是如果不用流水线,每次操作命令都需要建立网络连接,则就影响用户的体验,如果可以使用一次连接,操作多条命令,则大大节省了网络的开销。
应用场景:
Pipeline
是 Redis 的一个提高吞吐量的机制,适用于多 key 读写场景,比如同时读取多个key
的value
,或者更新多个key
的value
,并且允许一定比例的写入失败、实时性也没那么高,那么这种场景就可以使用了。比如 10000 条一下进入 redis,可能失败了 2 条无所谓,后期有补偿机制就行了,像短信群发这种场景,这时候用 pipeline 最好了。注意:
Pipeline
是非原子的,在上面原理解析那里已经说了就是 Redis 实际上还是一条一条的执行的,而执行命令是需要排队执行的,所以就会出现原子性问题。Pipeline
中包含的命令不要包含过多。Pipeline
每次只能作用在一个 Redis 节点上。Pipeline
不支持事务,因为命令是一条一条执行的。
-
-
与原生M操作对比
- 原生批量命令是原子的,Pipeline 是非原子的。
- 原生批量命令是一个命令对应多个 key,Pipeline 支持多个命令。
- 原生批量命令是 Redis 服务端支持实现的,而 Pipeline 需要服务端和客户端的共同实现
pipeline是将多条命令进行拆分,如有一万条命令,将其拆分成每100条进行发送,返回的结果是顺序的
原生M操作,即mget,是原子性的操作
-
客户端实现
-
使用建议
- 每次pipeline携带数据量
- pipeline每次只能作用在一个redis节点上
- M操作和pipeline区别
发布订阅
-
角色
发布者(publisher)
频道(channel)
订阅者(subscriber)
-
模型
发布订阅模式:订阅者都可以收到
redis进行消息的发布和订阅功能,在发送消息之前,但是订阅者还没有订阅,此时订阅者接收不到之前发布者发布的消息。
发布者:
publish channel message
redis > publish sohu:tv “hello world” (integer) 3 #订阅者个数 redis> publish sohu:auto "taxi' (integer)
订阅者
subscribe [channel] #一个或多个
redis> subscribe sohu:tv 1) "subscribe" 2) "sohu:tv" 3) (integer) 1
取消订阅
unsubscribe [channel] #一个或多个
消息队列模式:只要一个消息订阅者可以收到,是抢的模式,使用的是队列阻塞的形式