Redis那些事儿

388 阅读20分钟

简介

Redis是基于内存,可选持久性的key-value存储的NoSQL数据库

  • 高性能-读的速度是110,000次/s,写的速度是81,000次/s (基于内存,IO复用)

  • 原子性-单个操作都是原子性的,要么成功要么失败

安装

tar -zxvf redis-4.0.11.tar.gz
cd redis-4.0.11
make && make install 

默认命令安装在/usr/local/bin目录,列表如下

redis-cli  
redis-server
redis-sentinel 
redis-check-aof  
redis-check-rdb  
redis-benchmark  

核心指令

redis-server config_file_path --启动redis
redis-cli [-h host] [-p port] [-a password] --连接服务器

配置文件

推荐www.runoob.com/redis/redis…的参数说明

数据类型

每个key值最大为512M,针对key的命令如下

exists
type
del
expire
pexpire
ttl

String

set/get   
mset/mget
incry/decry
incryby/decryby

List

按插入顺序排序的字符串集合,底层通过LinkedList实现

rpush/lpush
rpop/lpop
rpoplpush
brpop/blpop
brpoplpush
lrang
ltrim

Hash

hset/hget
hmset/hmget
hincry/hdecry
hincryby/hdecryby

hdel
hexists
hlen
hkeys
hvals
hscan

Set

无序且不重复的字符串集和

sadd
scard
srem
smembers
sismember
sinter/sinerstore
sunion/sunionstore
sdiff/sdiffstore
sscan

sinterstore会将合并结果会存储在redis的新key中,sinter不会;其它类似

差集:返回第一个集合与其他集合之间的差异,也可以认为说第一个集合中独有的元素。
即结果来自第一个集和,但又不属于其它集和的元素
示例:
key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SDIFF key1 key2 key3 = {b,d}

sortedset

有序且不重复的字符串集和。每个字符串关联一个score值,值越小排序越前。如果score相同,则按照字符串的字典顺序排序,[字符串不能重复,score可以重复]

zadd
zrem
zcard   --返回有序集的成员个数
zcount  --分数值在min和max之间的成员个数
zincrby
zrang/zrangbyscore
zrerang/zrerangbysore
zscan

两者区别:

ZRANGE key start stop [WITHSCORES]
下标start stop 0表示有序集的第一个元素,1表示第二个,以此类推,-1表示倒数第一个,-2表示倒数第二个,以此类推

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
min max表示分数,区间不包含min,但是包含max,min最小是用-inf表示,max最大值用+inf表示
[LIMIT offset count] 类似 SQl中select * from a limit n,m,请注意如果offset很大,定
位offset需要遍历有序集,会有O(N)时间复杂度

BitMap

将String的值当做一系列bit来处理

setbit/getbit
bitcount

HyperLogLog

基数统计:一个集合中不重复元素的个数。例如集合 {1,2,3,1,2},它有5个元素,但它的基数/Distinct 数为3。

该数据结构统计结果不一定准确,会有0.7%的误差,但是每个键只占用12KB,理论可以添加2^64个元素

pfadd
pfcount
pfmeger

每个命令的用法个人推荐大牛博客:https://segmentfault.com/a/1190000020523110

持久化

支持rdb和aof持久化模式

RDB模式(快照)

保存的当前内存中的数据集。支持在“N秒内数据集至少有M个改动”条件满足的时候,就自动保持一次数据集。默认开始该模式。

自动触发:

#redis.conf的配置的条件如下
save 900 1
save 300 10
save 60 10000

手动触发:

BGSAVE

bgsave=fork+copyonwrite

当条件满足的时候,服务器工作如下:

  1. redis调用fork(),从父进程fork一个子进程
  2. 子进程将数据集写入到一个新的rdb文件
  3. 当子进程完成对新rdb文件写入的时候,redis会用新rdb文件替换旧rdb文件,并删除旧rdb文件

fork()出来的进程共享其父类的内存数据。仅仅是共享fork()出子进程的那一刻的内存数据,后期主进程修改数据对子进程不可见,同理,子进程修改的数据对主进程也不可见。

如何做到fork出来的子进程共享父进程当时的内存数据,而不是copy一份父进程当时的内存数据,并且之后相互之间不干扰呢?copy-on-write 即写时复制

AOF模式

保存每次改变数据集的命令追加到指定文件中。自动触发追加文件的策略:

  1. 每次出现改变数据集命令的时候就写入文件:非常慢,也非常安全
  2. 每秒写入:足够快,并且在故障时只会丢失 1 秒钟的数据。
  3. 从不写入:将数据交给操作系统来处理

该模式默认不开启,通过以下配置开启并指定触发策略

appendonly yes

# appendfsync always
appendfsync everysec
# appendfsync no
  • aof文件越来越大怎么办

现在命令都是追加到文件中,文件会越来越大,会占用一定的磁盘空间,并且redis重启恢复时间较长,如何解决呢?

aof文件支持重写机制,工作方式如下:

  1. redis调用fork(),从父进程fork一个子进程
  2. 子进程将内存中的数据集以最少命令表示被写入到新的aof文件中
  3. 对于新写入的命令,父进程一边写入到旧的aof文件中,一边记录到一个内存缓存中
  4. 当子进程重写完成后,发送信号通知父进程,父进程接收到信号后将内存缓存的数据追加到新aof文件中
  5. redis原子操作将新oaf文件替换旧aof文件,之后新写入的命令都追加到aof文件中

自动触发重写机制:

以下两个添加都满足的时候触发重写

##当前写入日志文件的大小超过上一次rewrite之后的文件大小的百分之100时就是2倍
(Redis会在记住最近一次重写后AOF文件的大小,如果启动以来未发生任何重写,则使用启动时AOF的大小)
auto-aof-rewrite-percentage 100
##当前文件大小要大于64MB
auto-aof-rewrite-min-size 64mb

手动触发重写机制:

BGREWRITEAOF 
  • aof文件损坏了怎么办

在正在写入aof文件的时候服务器停机,会造成aof文件出错,导致在重启redis时会拒绝加载aof文件,redis提供了redis-check-aof修复命令:

  1. 备份当前的aof文件
  2. redis-check-aof -fix aof文件
  3. 重启redis,等待载入修复后的aof文件恢复数据
  • 不小心执行了flushall怎么办

打开aof文件,找到最后一行的flushall命令,然后重启redis恢复数据

模式对比

  1. 相通数据集,aof文件比rdb大
  2. 从aof文件恢复恢复时间比从rdb文件恢复时间长
  3. aof可以开启每秒保存数据,比rdb更安全
  4. 同时开启两种模式,redis只加载aof文件
  5. 两种模式落盘都会fork子进程,fork过程是阻塞的,数据集越大阻塞时间越长

主从复制

主从复制主要的机制

1.当master和slave正常连接的时候,master通过命令流将数据集改变复制给slave,包括客户端写入、键的过期或驱逐

2.当slave和master断开连接后,重新连接上了会尝试进行部分重同步,即尝试获取断开连接时间段内丢失的命令流

3.当无法进行部分重同步的时候,slave会请求全量同步,master会创建所有数据的快照并发送给slave,之后将数据集更改时持续发送命令流到slave

主从复制工作(对三个机制的补充)

复制ID:每一个master都有一个replicationID,标志一个数据集

偏移量:每一个master都只有一个offset,标志自己发送了多少个字节的数据;即使没有slave,也会存在offset

  • 全量复制
  1. 在其内部有一条命令psync,是做同步的命令,它可以完成全量复制和部分复制的功能,当启动slave节点时,它会发送psync命令给主节点,从向主传递主节点的runid以及自己的偏移量
  2. master接收到命令后,判断传输的offset偏移量是否在buffer内,在则_增量复制_,否则继续3步骤
  3. master判断需要全量复制,主就会将自己的runid和offset传递给从
  4. slave节点保存master的基本信息
  5. master执行bgsave生成RDB文件,并且在此期间新产生的写入命令会被记录到repl_back_buffer(复制缓冲区)
  6. 主向从传输RDB文件
  7. 主向从发送复制缓冲区内容
  8. slave节点清空旧的数据
  9. slave节点加载RDB文件到内存中,同时加载缓冲区数据
  • 增量复制

如果发生类似抖动时候,可以有一种机制将这种损失降低到最低,如何实现的?

  1. 如果发生了抖动,相当于连接断开
  2. 主会将写命令记录到缓冲区,repl_back_buffer
  3. 当slave再次去连接master时候,会发送pysnc命令,将当前自己的offset和主的runid传递给master
  4. 如果发现传输的offset偏移量是在buffer内的,不在期间内就证明你已经错过了很多数据,buffer也是有限的,默认是1M,会将offset开始到队列结束的数据同步给从。这样master和slave就达到了一致

主从复制安全性

强烈建议同时开启master和slave的持久化功能。如果master未开启,redis服务重启十分危险。如下所示,缓存数据都被清空了:

1.设置节点 A 为 master 并关闭它的持久化设置,节点 B 和 C 从 节点 A 复制数据。

2.节点 A 崩溃,但是他有一些自动重启的系统可以重启进程。但是由于持久化被关闭了,节点重启后其数据集合为空。

3.节点 B 和 节点 C 会从节点 A 复制数据,但是节点 A 的数据集是空的,因此复制的结果是它们会销毁自身之前的数据副本。

主从复制处理过期key

slave 不会让 key 过期,而是等待 master 让 key 过期。当一个 master 让一个 key 到期(或由于 LRU 算法将之驱逐)时,它会合成一个 DEL 命令并传输到所有的 slave。

常用配置

  1. 一个master可以有多个slave,一个slave只能有一个master,数据是从master到slave单向的

  2. 设置主从命令slaveof master IP POST, 取消主从关系slaveof no one

  3. 设置slave只读slave-read-only  yes,只读模式下的 slave 将会拒绝所有写入命令

  4. 如果master节点设置了密码,slave节点配置masterauth

  5. 命令行连接上redis服务,通过info replication查看主从相关信息

    127.0.0.1:6081>info replication #Replication role:master connected_slaves:0 master_replid:1907e5fe95aa87ac27e71d8233be3b20be855243 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:2918 second_repl_offset:-1 repl_backlog_active:0 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:2918

哨兵模式

 yyy

集群模式

特性

  • 自动将数据分割到不同节点上

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽。

  • 整个集群的部分节点宕机能够继续处理命令(高可用)

为保证集群高可用。Redis集群采用主从复制模型,每个节点可以有多个从节点。主节点宕机,通过故障转移,从节点可以升级为主节点,继续提供服务。如果负责一段槽的主从节点都宕机,则集群不可用。

  • Redis 并不能保证数据的强一致性

其中一个原因是集群采用了异步复制

搭建

3主3从

  • 先启动6个单独的redis

    mkdir cluster-test cd cluster-test mkdir 7000 7001 7002 7003 7004 7005

    ###7000端口的redis配置如下,其它端口类似 port 7000
    cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes

    ###其它端口启动类似 cd 7000 ../redis-server ./redis.conf

  • 创建集群

(集群工具redis-trib位于Redis源码的src文件夹,该工具是ruby 程序,需安装ruby)

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
## -–replicas 1 表示希望为集群中的每个主节点创建一个从节点。
## 该命令一次只能执行一次,如果第二次执行会报如下错误:
## Node 127.0.0.1:7001 is not empty. Either the nodealready knows other nodes
## 解决方法是:删除rm -rf appendonly-*.aof dump-*.rdb nodes-*.conf

连接上redis服务,通过cluster nodes查看集群信息

127.0.0.1:7001> cluster nodes
0cbf0e47cd203db32d31dddb373beedb7f56106e 127.0.0.1:7004@17004 slave 1c16f6c0780101940beda5d795307b0a6fcbcf0f 0 1616517011139 4 connected
1c16f6c0780101940beda5d795307b0a6fcbcf0f 127.0.0.1:7001@17001 myself,master - 0 1616517012000 1 connected 0-5460
7b389646c468dd83b6f1810e3d16769f19595819 127.0.0.1:7006@17006 slave f116cae08e4f6538a4d2ebe6603b630829721123 0 1616517013000 6 connected
eecb6ce204542abc65118a0bfe416aa5576a2816 127.0.0.1:7005@17005 slave dcd77579360988bc3c184c5ce72510bbc962dba1 0 1616517012000 5 connected
dcd77579360988bc3c184c5ce72510bbc962dba1 127.0.0.1:7002@17002 master - 0 1616517014143 2 connected 5461-10922
f116cae08e4f6538a4d2ebe6603b630829721123 127.0.0.1:7003@17003 master - 0 1616517014000 3 connected 10923-16383
  • 重启集群(注意与创建集群的区别,重启方式缓存有值,创建集群方式则缓存无值)

    分别重新启动7000 7001 7002 7003 7004 7005端口

    ###其它端口启动类似 cd 7000 ../redis-server ./redis.conf

连接集群

redis-cli -c -p 7001 -a passwd
### -c表示连接的是集群,如果key不在当前节点的槽,则会转到其它redis节点

Q:执行创建集群命令redis-trib.rb报错可能是在redis中设置了集群密码

A:需要在ruby的cluster.rb中添加对应的密码

高级技术

键淘汰

过期淘汰

通过expire等命令设置的key过期时间,那么过期时间到了key是如何被redis淘汰的呢?

1.主动淘汰(惰性删除):在客户端访问某个key时候,key会被发现并主动过期

redis中的key并不一定都会被访问到,有些key可能永远不会被访问到,所以主动淘汰仅对会被访问到的key有作用。被动淘汰机制如下:

2.被动淘汰(定期删除):Redis每秒10次做的事情

  1. 测试随机的20个keys进行相关过期检测。
  2. 删除所有已经过期的keys。
  3. 如果有多于25%的keys过期,重复步奏1s

定期删除是选择随机keys进行的操作,会漏掉keys,这个时候就需要惰性删除了

惰性删除的时候有些keys到了过期时间没有被访问到,这个时候就需要定期删除了

内存淘汰

redis内存淘汰过多的时候,会进行内存淘汰,策略如下:

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,(一般没人用)

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用)

allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,一般没人用

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(一般不太合适)

volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间(剩余ttl小)的key优先移除

发布订阅

一种通信模式,发送者 (pub) 发送消息,订阅者 (sub) 接收消息。发布者不知道订阅者存在,订阅者不知道发布者存在,可以用于解耦。

subscribe/unsubscribe    --精确订阅频道,订阅多个用空格分开
psubscribe/punsubscribe  --模式订阅频道,订阅多个用空格分开
publish

管道技术

问题:客户端发送命令到服务端,服务端将响应回复给客户端。这里存在往返时间RTT。假设一个请求的RTT为500ms,即使服务器每秒能处理1w请求,每秒也只能处理2个请求。

Redis是一种基于客户端-服务端模型(C/S模型)以及请求/响应协议的TCP服务。客户端把命令发送到服务器,然后阻塞客户端,等待着从socket读取服务器的返回结果,然后再发下一个命令到服务器

方案:管道能在多个命令一次性发送到服务器,最后一个步骤中一次性读取该答复。

优点:管道的出现让我们在执行多行命令的时候仅消耗一次RTT时间,提高了性能。

缺点:管道和前面提到的mget、mset、hmget、hmset命令不同,后者每次只有一次RTT且能保证原子性,管道不能保证原子性。

应用场景

分布式缓存

从硬盘、数据库、网络获取数据比从缓存获取数据慢,将热点数据放到缓存中,应用程序从缓存获取可以提高程序的性能。

分布式队列

利用List数据结构的特性,各个客户端添加元素到列表中,消费端轮询或者阻塞的去消费列表元素,可以做到应用之间很好的解耦。

分布式锁

集群部署的应用,通过分布式锁来保护共享资源。

发布订阅

对于不常变的表数据,可以预先加载到缓存中,应用程序重缓存读取。应用程序在启动的时候订阅特定频道,如果表数据业务变更了,通过后管提供的发布入口去发布消息到频道,由应用程序重新加载数据到缓存。

布隆过滤器

判断一个元素是否在一个大集合中的高效方法。布隆过滤器的特点是:

1)一个元素不存在过滤器中就一定不存在,存在过滤器中有可能存在

2)占用内存小

3)判断是否存在速度快、效率高

利用该特性可以过滤掉绝大部分不存在的请求。比如黑名单,如果用户不存在过滤器中,就不是黑名单用户;如果存在过滤其中,则需要从数据源获取数据再次判断是否存在。

问题与方案

缓存三大问题

先查询缓存,再查询数据库

缓存穿透

Q:查询缓存没有、数据库也没有的数据。可以利用这种一定不存在的数据查询来攻击系统,所有查询都会打到DB,导致DB瘫痪而引起故障

A1:使用布隆过滤器,将存在的数据加载到布隆过滤器中,在应用层完成过滤

A2:回种空值,设置一定时间的有效期

缓存雪崩

Q:缓存中大量的key在同一时间失效,同一时间大量的查询请求无法从缓存获取值而打到DB,导致DB瘫痪而引起故障

A1:添加分布式锁,线程互斥,只有一个线程去获取数据,其它线程等待。明显的缺点是降低了系统的QPS

A2:错开失效时间:在缓存进行失效时间设置的时候,从某个适当的值域中随机一个时间作为失效时间即可

缓存击穿

Q:缓存击穿是缓存雪崩的一个特例,针对“热点数据”失效,也会打到DB,在请求量很大的情况下,导致DB瘫痪而引起故障

A1:对热点数据不设置过期时间,永久有效

A2:使用分级缓存

采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程能获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程因为获取不到锁从 L2 缓存获取数据并返回。

这种方式,主要是通过避免缓存同时失效并结合锁机制实现。数据表中的数据和L2 缓存中
可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案 可能会造成额外的缓存空间浪费。

连接数满了

Q:

A:

应用起不来

Q:

A:

keys * 能用

Q:keys * 相当于数据库的select * form table查找所有数据,并且redis是单线程执行的,这个命令花费时间越久,阻塞时间就越久,会导致应用不可用

A:scan命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 scan命令的游标参数, 以此来延续之前的迭代过程。

SCAN cursor [MATCH pattern] [COUNT count]
##参数说明
cursor - 游标
pattern - 匹配的模式
count - 指定从数据集里返回多少元素,默认值为10

大key操作

  • 寻找大key

可以通过redis-rdb-tools工具分析rdb文件寻找大key可以使用工具

核心命令: rdb  -c  command  [ 可选参数]  rdb文件

###查看命令使用帮助
rdb --help
###以json格式输出
rdb -c json dump.rdb  
###生成内存报告
rdb -c memory dump.rdb 
###对比rdb文件
rdb -c diff dump1.rdb |sort > dump1.txt
rdb -c diff dump2.rdb |sort > dump2.txt
使用对比工具对比dump1.txt和dump2.txt文件
###找出单个key的内存使用情况
redis-memory-for-key [-h host] [-p port] [-a pwd] key

可选参数为过滤作用,如下:
--db [0-16] 
--type [string|set|sortedset|hash]
--key 'a.*'  匹配a开头的key,末尾以.*表示匹配一个或多个字符
--byte x 字节数大于等于x的
--large N 前N个最大的key
  • 删除大key

1、Hash 删除: hscan + hdel  先通过hscan迭代出要删除的key,然后通过hdel循环删除

2、List 删除: ltrim    在循环体里面每次截掉左侧1000个key

3、Set 删除: sscan + srem   先通过sscan迭代出要删除的key,然后通过srem循环删除

4、SortedSet 删除: zscan + zrem 先通过zscan迭代出要删除的key,然后通过zrem循环删除

线程模型

xxx