Redis
Redis
Redis简介及使用场景
本文仅做学习记录
简介
Redis是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的APl。
本质是客户端服务端应用软件程序。
特点是使用简单、性能强悍、功能应用场景丰富。
官网地址:Redis
使用场景
- 微博数,粉丝数(常规计数)
- 微博的关注列表,粉丝列表
- 实现如共同关注、共同喜好、二度好友
- 排行榜、按照用户投票和时间排序
- 存储部分变更数据,如用户信息
- LBS应用开发
- 消费者生产者场景(类似MQ)
注:在工作中,redis用于流式计算中,用来存储计算数据加快程序处理效率;记录实时数据等。
操作环境
系统 | Docker | Redis版本 |
---|---|---|
MAC | 2.1.0.0 | 5.0.5 |
Redis基本数据结构及常用命令
通用命令
命令 | 描述 |
---|---|
DEL KEY | 该命令用于在key存在时删除key |
EXISTS key | 检查给定key是否存在 |
EXPIRE key seconds | 为给定key设置过期时间,以秒计 |
TTL key | 以秒为单位,返回给定key的剩余生存时间(TTL, time to live) |
TYPE key | 返回key所储存的值的类型 |
127.0.0.1:6379> set test 123
OK
127.0.0.1:6379> EXISTS test
(integer) 1
127.0.0.1:6379> EXPIRE test 20
(integer) 1
127.0.0.1:6379> TTL test
(integer) 15
127.0.0.1:6379> type test
string
String:数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
使用场景:微博数,粉丝数(常规计数)
命令 | 描述 |
---|---|
Get | 获取指定key的值 |
Set | 设置指定key的值 |
Incr | 将key中储存的数字值增一 |
Decr | 将key中储存的数字值减一 |
Mget | 获取所有(一个或多个)给定key的值 |
127.0.0.1:6379> set test 0
OK
127.0.0.1:6379> get test
"0"
127.0.0.1:6379> INCR test
(integer) 1
127.0.0.1:6379> get test
"1"
127.0.0.1:6379> DECR test
(integer) 0
127.0.0.1:6379> set test1 5
OK
127.0.0.1:6379> MGET test test1
1) "0"
2) "5"
List:就是链表,相信略有数据结构知识的人都应该能理解其结构。
使用场景:微博的关注列表,粉丝列表
命令 | 描述 |
---|---|
Lpush | 将一个或多个值插入到列表头部 |
Rpush | 在列表中添加一个或多个值 |
Lpop | 移出并获取列表的第一个元素 |
Rpop | 移除列表的最后一个元素,返回值为移除的元素 |
Lrange | 获取所有(一个或多个)给定key的值 |
127.0.0.1:6379> LPUSH test aaa
(integer) 1
127.0.0.1:6379> RPUSH test bbb
(integer) 2
127.0.0.1:6379> LRANGE test 0 -1
1) "aaa"
2) "bbb"
127.0.0.1:6379> RPOP test
"bbb"
127.0.0.1:6379> LPOP test
"aaa"
127.0.0.1:6379> LRANGE test 0 -1
(empty list or set)
Set:就是一一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的Set数据结构,可以存储一些集合性的数据。
使用场景:实现如共同关注、共同喜好、二度好友
命令 | 描述 |
---|---|
Sadd | 向集合中添加一个或多个成员 |
Spop | 移除并返回集合中的一个随机元素 |
Smembers | 返回集合中的所有成员 |
Sunion | 返回所有给定集合的并集 |
127.0.0.1:6379> SADD list1 1111
(integer) 1
127.0.0.1:6379> SADD list1 2222
(integer) 1
127.0.0.1:6379> SADD list1 3333
(integer) 1
127.0.0.1:6379> SMEMBERS list1
1) "1111"
2) "2222"
3) "3333"
127.0.0.1:6379> SPOP list1 2
1) "3333"
2) "1111"
127.0.0.1:6379> SMEMBERS list1
1) "2222"
127.0.0.1:6379> SADD list2 1111
(integer) 1
127.0.0.1:6379> SADD list2 2222
(integer) 1
127.0.0.1:6379> SUNION list1 list2
1) "1111"
2) "2222"
Sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。
使用场景:排行榜、按照用户投票和时间排序
命令 | 描述 |
---|---|
Zadd | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
Zrange | 通过索引区间返回有序集合中指定区间内的成员 |
Zrem | 移除有序集合中的一个或多个成员 |
Zcard | 获取有序集合的成员数 |
127.0.0.1:6379> ZADD test 10 s1
(integer) 1
127.0.0.1:6379> ZADD test 50 s2
(integer) 1
127.0.0.1:6379> ZADD test 90 s3
(integer) 1
127.0.0.1:6379> ZCARD test
(integer) 3
127.0.0.1:6379> ZRANGE test 0 -1
1) "s1"
2) "s2"
3) "s3"
127.0.0.1:6379> ZRANGE test 0 1
1) "s1"
2) "s2"
127.0.0.1:6379> ZREM test s2
(integer) 1
127.0.0.1:6379> ZRANGE test 0 -1
1) "s1"
2) "s3"
Hash
Hash是一个string类 型的field和value的映射表
使用场景:存储部分变更数据,如用户信息
命令 | 描述 |
---|---|
Hget | 获取存储在哈希表中指定字段的值 |
Hset | 将哈希表key中的字段field的值设为value |
Hgetall | 获取在哈希表中指定key的所有字段和值 |
127.0.0.1:6379> HSET test field "aaaa"
(integer) 1
127.0.0.1:6379> HGET test field
"aaaa"
127.0.0.1:6379> HSET test field1 "bbbb"
(integer) 1
127.0.0.1:6379> HSET test field2 "cccc"
(integer) 1
127.0.0.1:6379> HGETALL test
1) "field"
2) "aaaa"
3) "field1"
4) "bbbb"
5) "field2"
6) "cccc"
127.0.0.1:6379> HDEL test field
(integer) 1
127.0.0.1:6379> HGETALL test
1) "field1"
2) "bbbb"
3) "field2"
4) "cccc"
GEO
GEO 3.2版本开始对GEO(地理位置)的支持
使用场景: LBS应用开发
命令 | 描述 |
---|---|
GEOADD | 增加地理位置的坐标,可以批量添加地理位置 |
GEODIST | 获取两个地理位置的距离 |
GEOHASH | 获取某个地理位置的geohash值 |
GEOPOS | 获取指定位置的坐标,可以批量获取多个地理位置的坐标 |
GEORADIUS | 根据给定地理位置坐标获取指定范围内的地理位置集合 |
GEORADIUSBYMEMBER | 根据给定成员的位置获取指定范围内的位置信息集合 |
(注意:该命令的中心点是由给定的位置元素决定)
127.0.0.1:6379> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
127.0.0.1:6379> GEODIST Sicily Palermo Catania
"166274.1516"
127.0.0.1:6379> GEORADIUS Sicily 15 37 100 km
1) "Catania"
127.0.0.1:6379> GEORADIUS Sicily 15 37 200 km
1) "Palermo"
2) "Catania"
127.0.0.1:6379> GEORADIUSBYMEMBER Sicily Palermo 2000 km
1) "Palermo"
2) "Catania"
Stream
Stream 5.0版本开始的新结构“流”
使用场景:消费者生产者场景(类似MQ)
命令 | 描述 |
---|---|
XADD | 往指定的流中添加消息 |
XLEN | stream流中的消息数量 |
XDEL | 删除流中的消息 |
XRANGE | 返回流中满足给定ID范围的消息 |
XREAD | 从一个或者多个流中读取消息 |
XINFO | 检索关于流和关联的消费者组的不同的信息 |
这种类型还没有实际使用过,没有去研究,有兴趣的可以去官网看看。
注:比较常用的几个:Hash、String、Set、List、Sorted set。
持久化
RBD持久化
RDB持久化方式能够在指定的时间间隔对你的数据进行快照存储。
客户端直接通过命令BGSAVE或者SAVE来创建一个内存快照
- BGSAVE调用fork来创建一个子进程,子进程负责将快照写入磁盘,而父进程任然继续处理命令。
- SAVE执行SAVE命令过程汇总,不再相应其他命令。
在redis.conf中调整save配置选项,当在规定的时间内,Redis发生了写操作的个数满足条件会触发发生BGSAVE命令。
#900 秒之内至少一次写操作
save 900 1
#300 秒之内至少发生10次写操作
save 300 10
#60 秒之内发生至少10000次
save 60 10000
RDB文件进行数据恢复比使用AOF要快很多
AOF(append only file)持久化
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来回复原始数据。
开启AOF持久化
appendonly yes
AOF策略调整
#每次有数据修改发生时都会写入AOF文件
appendfsync always
#每秒钟同步一次,该策略为AOF的缺省策略
appendfsync everysec
#从不同步。高效但是数据不会被持久化
appendfsync no
内存分配
不同数据类型的大小限制
- Strings类型: 一个String类型的value最大可以存储512M。
- Lists类型: list的元素个数最多为2^32-1个,也就是4294967295个。
- Sets类型:元素个数最多为2^32-1个,也就是4294967295个。
- Hashes类型:键值对个数最多为2^32-1个,也就是4294967295个
最大内存控制
maxmemory最大内存阈值
maxmemory-policy到达阈值的执行策略
内存压缩
#配置字段最多512个
hash-max-zipmap-entries 512
#配置value最大为64字节
hash-max-zipmap-value 64
#配置元素个数最多512个
list-max-ziplist-entries 512
#配置value最大为64字节
list-max-ziplist-value 64
#配置元素个数最多512个
set-max-intset-entries 512
#配置元素个数最多128个
zset-max-ziplist-entries 128
#配置value最大为64字节
zset-max-ziplist-value 64
注:大小超出压缩范围,溢出后Redis将自动将其转换为正常大小
过期数据的处理策略
主动处理( redis主动触发检测key是否过期)每秒执行10次。 过程如下:
1.从具有相关过期的密钥集中测试20个随机密钥
2.删除找到的所有密钥已过期
3.如果超过25%的密钥已过期,请从步骤1重新开始
被动处理:
1.每次访问key的时候,发现超时后被动过期,清理掉
数据恢复阶段过期数据的处理策略
RDB方式:
- 过期的可以不被持久化到文件中。
- 载入时过期的key,会通过redis的主动和被动方式清理掉。
AOF方式: - 当redis使用AOF方式持久化时,每次遇到过期的key redis会追加一条DEL命令到AOF文件,也就是说只要我们顺序载入执行AOF命令文件就会删除过期的键。
注:过期数据的计算和计算机本身的时间是有直接联系的!
Redis内存回收策略
配置文件中设置: maxmemory-policy noeviction
动态调整: config set maxmemory-policy noeviction
回收策略 | 说明 |
---|---|
noeviction | 客户端尝试执行会让更多内存被使用的命令直接报错 |
allkeys-lru | 在所有key里执行LRU算法 |
volatile-lru | 在所有已经过期的key里执行LRU算法 |
volatile-lfu | 使用过期集在密钥中使用近似LFU进行驱逐 |
allkeys-lfu | 使用近似LFU逐出任何键 |
allkeys-random | 在所有key里随机回收 |
volatile-random | 在已经过期的key里随机回收 |
volatile-ttl | 回收已经过期的key,并且优先回收存活时间(TTL) 较短的键 |
LRU算法
LRU (Least recently used,最近最少使用) :根据数据的历史访问记录来进行淘汰数据
- 核心思想:如果数据最近被访问过,那么将来被访问的几率也更高。
- 注意: Redis的LRU算法并非完整的实现,完整的LRU实现是因为这需要太多的内存。
- 方法:通过对少量keys进行取样(50%),然后回收其中一个最好的key。
- 配置方式:maxmemory-samples 5
LFU算法
LFU (Least Frequently Used)根据数据的历史访问频率来淘汰数据
- 核心思想:如果数据过去被访问多次,那么将来被访问的频率也更高。
- Redis实现的是近似的实现,每次对key进行访问时,用基于概率的对数计数器来记录访问次数,同时这个计数器会随着时间推移而减小。
- Morris counter算法依据:https://en.wikipedia.org/wiki/Approximate_counting_algorithm
- 启用LFU算法后,可以使用热点数据分析功能。 ( redis-cli --hotkeys )
主从复制
为什么要使用主从复制?
- redis-server单点故障
- 单节点QPS有限
主从复制应用场景分析
- 读写分离场景,规避redis单机瓶颈
- 故障切换,master出现问题后还有slave节点可以使用
搭建主从复制
第一种方式:命令行
#连接需要实现从节点的redis,执行下面的命令
slaveof [ip] [port]
第二种方式:redis.conf配置文件
#配置文件中增加
slaveof [ip] [port] 新版本:replicationof
#从服务器是否只读(默认yes)
slave-read-only yes
退出主从集群的方式
Slaveof no one
主从复制流程
- 从服务器通过psync命令发送服务器已有的同步进度(同步源ID、同步进度offset)
- master收到请求,同步源为当前master,则根据偏移量增量同步
- 同步源非当前master,则进入全量同步: master生 成rdb,传输到slave,加载到slave内存
主从复制核心知识
- Redis默认使用异步复制,slave和master之间异步地确认处理的数据量。
- 一个master可以拥有多个slave。
- slave可以接受其他slave的连接。slave可以有sub slave。
- 主从同步过程在master侧是非阻塞的。
- slave初次同步需要删除旧数据,加载新数据,会阻塞到来的连接请求。
主从复制应用场景
- 主从复制可以用来支持读写分离。
- slave服务器设定为只读,可以用在数据安全的场景下。
- 可以使用主从复制来避免master持久化造成的开销。master关闭持久化,slave配置为不定期保存或是启用AOF。(注意:重新启动的master程序将从一个空数据集开始,如果一个slave试图与它同步,那么这个slave也会被清空。)
主从复制的注意事项
读写分离场景:
- 数据复制延时导致读到过期数据或者读不到数据(网络原因、slave阻塞)
- 从节点故障(多个client如何迁移)
全量复制情况下:
- 第一次建立主从关系或者runid不匹配会导致全量复制
- 故障转移的时候也会出现全量复制
复制风暴:
- master故障重启,如果slave节点较多,所有slave都要复制,对服务器的性能,网络的压力都会有很大的影响。
- 如果一个机器部署了多个master
写能力有限:
- 主从复制还是只有一台master,提供的写服务能力有限。
master故障情况下:
- 如果是master无持久化,slave开启持久化来保留数据的场景,建议不要配置redis自动重启。
- 启动redis自动重启,master启动后,无备份数据,可能导致集群数据丢失的情况。
带有效期的key:
- slave不会让key过期,而是等待master让key过期。
- 在Lua脚本执行期间,不执行任何key过期操作。
Redis集群分片存储
1、客户端发送数据到redis集群中的时候,redis服务器端会利用crc16算法计算hash数据,如果请求的redis服务器不是目标服务器,那么会重定向到目标服务器,找到相应的slot(槽、位置)。——存在问题:如果说发送错误的次数过多,那么性能会下降。
解决办法:可以在发送命令之前,客户端进行slot计算,根据slot分配信息定位到目标服务器,如果说redis服务器返回重定向消息的时候,那么就说明slot分配信息改变,需要重新刷新缓存信息。
注:slot并不是存储数据的,而是作为片区的划分
集群关心的问题
-
增加了slot槽的计算,是不是比单机性能差?
共16384个槽, slots槽计算方式公开的,HASH_ SLOT = CRC16(key) mod 16384。
为了避免每次都需要服务器计算重定向,优秀的java客户端都实现了本地计算,并且缓存服务器slots分配,有变动时再更新本地内容,从而避免了多次重定向带来的性能损耗。(结合画图过程理解) -
redis集群大小,到底可以装多少数据?
理论是可以做到16384个槽,每个槽对应一个实例,但是redis官方建议是最大1000个实例。存储足够大了。 -
集群节点间是怎么通信的?
每个Redis群集节点都有一个额外的TCP端口,每个节点使用TCP连接与每个其他节点连接。检测和故障转移这些步骤基本和哨兵模式类似(毕竟是同一个软件,同一个作者设计)。 -
ask和moved重 定向的区别
重定向包括两种情况:- 若确定slot不属于当前节点,redis会 返回moved。
- 若当前redis节点正在处理slot迁移,则代表此处请求对应的key暂时不在此节点,返回ask,告诉客户端本次请求重定向。
-
数据倾斜和访问倾斜的问题
倾斜导致集群中部分节点数据多,压力大。解决方案分为前期和后期:- 前期是业务层面提前预测,哪些key是热点,在设计的过程中规避。
- 后期是slot迁移,尽量将压力分摊(slot调整有自动rebalance、reshard和手动) 。
-
slot手动迁移怎么做?
迁移过程如下,大致描述如下:- 在迁移目的节点执行cluster setslot IMPORTING 命令,指明需要迁移的slot和迁移源节点。
- 在迁移源节点执行cluster setslot MIGRATING <node |D>命令,指明需要迁移的slot和迁移目的节点。
- 在迁移源节点执行cluster getkeysinslot获取该slot的key列表。
- 在迁移源节点执行对每个key执行migrate命令,该命令会同步把该key迁移到目的节点。.
- 在迁移源节点反复执行cluster getkeysinslot命令,直到该slot的列表为空。
- 在迁移源节点和目的节点执行cluster setslot NODE ,完成迁移操作。
-
节点之间会交换信息,传递的消息包括槽的信息,带来带宽消耗。
注意:避免使用大的一个集群,可以分多个集群。 -
Pub/Sub发布订 阅机制
注意:对集群内任意的一-个节点执行publish发布消息,这个消息会在集群中进行传播,其他节点接收到发布的消息。 -
读写分离
- redis-cluster默认所有从节点上的读写,都会重定向到key对接槽的主节点上。
- 可以通过readonly设置当前连接可读,通过readwrite取消当前连接的可读状态。
注意:主从节点依然存在数据不一致的问题
未完待续…