文章目录
- Redis数据库(端口:6379)
Redis数据库(端口:6379)
1. 什么是Redis
Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
Redis的优势:
- 性能极高:Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型:Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子性:Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性:Redis还支持 publish/subscribe, 通知, key 过期等等特性。
Redis与其他key-value存储有什么不同
-
Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
-
Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
2. redis的数据类型
- 字符串(String)
string类型是二进制安全的,可以包含任何数据,比如JPG图片或者序列化对象。string类型是Redis最基本的数据类型,一个键最大能存储512MB。
# 设置字符串类型
127.0.0.1:6379> set key value
# 读取字符串类型
127.0.0.1:6379> get key value
- 哈希(Hash)
hash存的是字符串和字符串值之间的映射,是一个键值(key=>value)对集合,比如用户存储姓名、年龄等信息,非常适合存储对象。
# 建立哈希,并赋值
127.0.0.1:6379> hmset name key1 value1 key2 value2 ...
127.0.0.1:6379> hmset user:001 username liming pwd 123456 age 18
# 列出哈希的内容
127.0.0.1:6379> hgetall user:001
# 输出结果
"username"
"liming"
"pwd"
"123456"
"age"
"18"
# 更改哈希中的某一个值
127.0.0.1:6379> hgetall user:001 pwd aaaaaa
# 查看更改后的哈希的内容
127.0.0.1:6379> hgetall user:001
# 输出结果
"username"
"liming"
"pwd"
"aaaaaa"
"age"
"18"
- 列表(List)
Redis 列表是简单的字符串列表,按照插入顺序排序,底层实现是链表,对于一个具有几百上千万个元素的list来说,在头部和尾部插入一个新元素,其时间复杂度是常数级别的,插入速度也是不变的。弊端就是链表型的list比数组型的list定位更慢。list主要应用在消息队列上,可以确保先后顺序,还可以利用
lrange
实现分页功能,博客系统中评论也可以存入一个单独的list中。
# 头部插入元素
127.0.0.1:6379> lpush mylist "1"
# 尾部插入元素
127.0.0.1:6379> rpush mylist "2"
# 读取list中的元素
127.0.0.1:6379> lrange mylist index1 index2 ...
# 读取list中编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
# 删除元素
127.0.0.1:6379> lrem mylist index
# 获取list长度
127.0.0.1:6379> llen mylist
- 集合(Set)
Redis的Set是string类型的无序且元素不重复的集合,集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1),同时可以进行集合运算:取交集、取并集、取差集等。
# 添加元素
127.0.0.1:6379> sadd myset "1"
# 读取元素
127.0.0.1:6379> smembers myset
# 删除元素
127.0.0.1:6379> srem myset "1"
# 判断元素是否存在,存在返回1,否则返回0
127.0.0.1:6379> sismember myset "1"
# 取交集
127.0.0.1:6379> sinter set1 set2
# 取并集
127.0.0.1:6379> sunion set1 set2
# 取差集
127.0.0.1:6379> sdiff set1 set2
- 有序集合(sorted set)
有序集合中,每个元素都会关联一个double类型的分数(score),redis正是通过分数来为集合中的元素进行从小到大的排序。有序集合中的元素是唯一的,但分数(score)可以重复。
# 添加元素,赋予序号1
127.0.0.1:6379> zadd myzset 1 aa
# 添加元素,赋予序号2
127.0.0.1:6379> zadd myzset 2 bb
# 添加元素,赋予序号3
127.0.0.1:6379> zadd myzset 3 cc
# 读取元素,并输出序号(即score)
127.0.0.1:6379> zrange myzset 0 -1 with scores
# 得出排名
127.0.0.1:6379> zrange myzset cc
3. 两种持久化方式
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
- RDB(Redis DataBase)
RDB是Redis默认的方式,就是把某一时刻的数据以快照的方式存储到磁盘等介质上。Redis在进行持久化过程中,会调用rdbSave()函数,把数据写到临时文件中,等持久化过程都结束了,才调用rdbLoad()函数加载上次持久化好的文件,并用新的临时文件进行覆盖,快照文件总是完整的。
Redis会单独创建一个子进程进行持久化,而主进程是不会进行任何IO操作,确保了Redis极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。但是如果对数据的完整性非常敏感,RDB方式安全性低一些,可能在故障时丢失数据。
- AOF(Append Only File)
Append Only File意思是只允许最佳不允许改写的文件,AOF的方式是将执行过程的所有写指令记录下来,在数据回复时按照冲钱到后顺序在将指令执行一遍。通过配置redis.conf中的
appendonly yes
就可以打开AOF功能,每当服务器执行定时任务或者函数时,都会调用flushAppendOnlyFile()
函数,根据条件将aof_buf中的缓存写到AOF文件中,而后调用fsync()
或fdatasync()
函数将AOF保存到磁盘中。其中AOF默认持久化的策略是每秒钟fsync一次,所以对数据安全更加有保障。
因为采用了追加的方式,不做处理的话,AOF文件将会越来越大,所以要使用AOF文件重写机制,当AOF文件大小超过说设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。如100条INCR指令可以合并层一条SET指令。
在进行AOF重写时,仍然先写入临时文件,全部完成后再进行替换,因此断电、磁盘满等问题都不会影响AOF文件的可用性。
在同样的数据规模下,AOF文件比RDB文件的体积更大,且AOF方式恢复数据的速度也比RDB方式慢。
4. RDB与AOF比较
- AOF文件比RDB更新更加频繁,因此AOF文件更大
- AOF方式比RDB方式数据安全性更高
- RDB性能比AOF更好
- 优先使用AOF还原数据
5. 如果AOF文件出现被写坏的情况,该怎么处理
出现AOF文件被写坏的情况,Redis并不会冒然加载这个有问题的AOF文件,而是报错退出,这时可以用过以下步骤来修复出错文件:
- 备份被写坏的AOF文件
- 运行
redis-check-aof -fix
进行修复 - 用
diff -u
比两个文件的差异,定位问题点并修复 - 重启redis,加载修复后的AOF文件
6. redis 和 memcached 的主要区别
- Redis支持服务器端的数据操作:相比于memcached,redis拥有更多是数据结构,所以支持更多的数据操作,而在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。
- 内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
- 性能对比:redis只支持单核,memcached可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高,但是在大数据存储上的处理memcached性能更高。
- redis内部使用的是文件事件处理器
file event handler
,是单线程模型,处理效率高。文件事件处理器的结构包含多个socket、IO多路复用程序(非阻塞)、文件事件分派器、事件处理器四个部分。 - 存储数据安全:Redis拥有持久化机制,而memcached没有。
- 应用场景:Redis除了作为NoSQL数据库使用外,还能做消息队列、数据堆栈和数据缓存等;而Memcached适用于缓存SQL语句、数据集、用户临时性数据、延迟查询数据和session等。
7. 事务处理
Redis 事务可以一次执行多个命令,并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中
一个事务从开始到执行会经历开始事务、命令入队、执行事务三个阶段。
redis 通过 MULTI、EXEC、DISCARD、WATCH四个指令实现事务处理
- MULTI用来组装一个事务
- EXEC用来执行一个事务
- DISCARD用来取消一个事务
- WATCH用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行
8. Redis常见的架构模式
(1) 单机模式
- 内存容量有限
- 处理能力有限
- 无法高可用
(2) 主从复制模式
MySQL一样,redis是支持主从同步的,而且也支持一主多从以及多级从结构。一是为了纯粹的冗余备份,二是为了提升读性能。
通常我们会设置一个主节点,N个从节点,默认情况下,主节点负责处理使用者的IO操作,而从节点则会对主节点的数据进行备份,并且也会对外提供读操作的处理。
主从架构中,可以考虑关闭主服务器的数据持久化功能,只让从服务器进行持久化,这样可以提高主服务器的处理性能。
Redis的主从同步是异步进行的,不会阻塞主节点,主节点和从节点是读写分离的。
全量同步:Redis全量复制一般发生在slave初始化阶段,这时,需要将Master上所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令
- 主服务器接收到SYNC命令后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令
- 从服务器完成对快照的载入,开始接受命令请求 ,并执行来自主服务器缓冲区的写命令
增量同步:通常情况下,Master每执行一个写命令就回向Slave发送相同的命令,然后slave接收并执行。只有slave第一次连接是全量同步,断线重连有可能是全量同步,也有可能是增量同步;除此之外都是增量同步。
Redis主从复制模式的优点:
- 解决数据备份问题
- 实现读写分离,降低读压力,提高服务器性能
Redis主从复制模式的缺点:
- 无法实现高可用,一旦出现故障需要人工进行故障转移
- 无法实现动态扩容,受单击控制
(3) 哨兵模式
sentinel是基于主从模式进行优化,能够为Redis提供高可用性。在实际生产中,服务器难免遇到服务器宕机、停电、硬件损坏等突发状况,哨兵模式可以一定程度上帮助规避这些意外情况导致的灾难性的后果。核心还是主从复制,增加了由一个或多个Sentinel实例组成的Sentinel系统,监视任意多个主服务器以及这些主服务器属下的所有从服务器,当主服务器宕机导致不可写或者下线状态时,自动将主服务器下的某个服务器升级为新的主服务器。这样,就保证了Redis的高可用,规避数据丢失风险。
sentinel的特点:
- 监控(Monitoring):它会监听不断检查主服务器和从服务器之间是否在正常工作。
- 通知(Notification):当监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理管或者其他应用程序发送通知。
- 故障自动转移(Automatic failover):当一个主服务器出现故障不能正常工作时,Sentinel会在所有从服务器中选择一个作为新的主服务器,实现故障自动转移。
- 提供主服务器地址:能够向当前使用者提供当前主节点的地址,特别是在故障自动转移后,使用者不用做任何修改就可以知道当前的主节点地址。
- sentinel也可以集群,部署多个哨兵,sentinel可以通过发布和订阅(pub/sub)来自动发现Redis集群上的其他Sentinel。sentinel在发现其它sentinel进程后,会将其放入一个列表中,这个列表存储了所有已被发现的sentinel。
Redis哨兵模式的优点:
- 保证高可用
- 监控各个节点
- 自动故障迁移
Redis哨兵模式的缺点:
- 切换需要时间,可能存在丢失数据风险
- 从节点下线,故障不能自动转移
- 无法实现动态扩容
(4) 集群模式
Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。
Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。
Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,比如当前集群有3个节点,那么节点 A 包含 0 到 5500号哈希槽;节点 B 包含5501 到 11000 号哈希槽;节点 C 包含11001 到 16384号哈希槽;这种结构很容易添加或者删除节点,比如如果我想新添加个节点D,我需要从节点 A, B, C中得部分槽到D上。如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
一致哈希算法根据数据的key值计算映射位置时和所使用的节点数量有非常大的关系。一致哈希分区的实现思路是为系统中每个节点分配一个token,范围一般在0~2^32,这些token构成一个哈希环,数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该hash值的token节点,需要操作的数据就保存在该节点上。
Redis 一致性保证 Redis 并不能保证数据的强一致性。这意味这在实际中集群在特定的条件下可能会丢失写操作。
投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉。这时如果当前master没有slave,集群就进入fail状态;当集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。
一般集群建议搭建三主三从架构,三主提供服务,三从提供备份功能。
Redis集群模式的优点:
- 有效解决了Redis在分布式方面的需求
- 遇到单击内存,并发和流量瓶颈问题时,可采用Cluster方案达到负载均衡的目的
- 可实现动态扩容,可线性扩展到1000个节点,节点可以动态的添加或删除
- P2P模式,无中心化
- 自动故障转移,节点间通过Gossip协议同步节点信息,用投票机制完成slave到maser的角色提升
- 数据按照Slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 高可用性,部分节点不可用时,集群仍然可用,通过增加slave做备份数据副本
Redis集群模式的缺点:
- 架构比较新,最佳时间比较少
- 为了性能提升,客户端需要缓存路由表信息
- 节点发现、reshard操作不够自动化
9. Redis如何实现分布式锁
使用 setnx 实现枷锁,可以同时通过 expire 添加超时时间
# 当key不存在时,为key设置一个值,否则在一定时间内进行尝试
def acquire_lock(conn, lockname, acquire_timeout=10):
identifier = str(uuid.uuid4())
end = time.time() + acquire_timeout
while time.time() < end:
if conn.setnx('lock:' + lockname, identifier):
return identifier
time.sleep(0.001)
return False
锁的 value 值可以使用一个随机的 uuid 或者特定的命名
释放锁的时候,通过 uuid 判断是否是该锁,是则执行 delete 释放锁
def release_lock(conn, lockname,identifier):
pipe = conn.pipeline(True)
lockname = 'lock:' + lockname
while True:
try:
pipe.watch(lockname)
if pipe.get(lockname) == identifier:
pipe.multi()
pipe.delete(lockname)
pipe.execute()
return True
pipe.unwatch()
break
except redis.exceptions.WatcjError:
pass
return False
10. 缓存使用场景
缓解关系数据库并发访问的压力: 热点数据
减少响应时间:内存IO速度比磁盘快
提升吞吐量:redis等内存数据库单机就可以支撑很大并发
11. 常用的缓存使用模式
- Cache-Aside
Cache-Aside可能是项目中最常见的一种模式。它是一种控制逻辑都实现在应用程序中的模式。缓存不和数据库直接进行交互,而是由应用程序来同时和缓存以及数据库打交道,即同时更新缓存和数据库。
应用场景:应用于缓存不支持Read-Through/Write-Through的系统。
优点:缓存仅仅保存被请求的数据,属于懒加载模式,避免了任何数据都被写入缓存造成缓存频繁的更新。
缺点:当发生缓存未命中的情况时,则会比较慢,因为要经过三个步骤,查询缓存、从数据库读取、写入缓存;复杂的逻辑都在应用程序中,如果实现微服务,多个微服务中会有重复的逻辑代码。
- Read-Through / Write Through
这种模式中,应用程序将缓存作为主要的数据源,而数据库对于应用程序是透明的,更新数据库和从数据库的读取的任务都交给缓存来代理了,所以对于应用程序来说,简单很多。先更新缓存,缓存负责同步更新数据库。
应用场景:写入之后经常被读取的应用。
优点:缓存不存在脏数据;相比较Cache-Aside懒加载模式,读取速度更高,因为较少因为缓存未命中而从数据库中查找;应用程序的逻辑相对简单。
缺点:对于总是写入却很少被读取的应用,那么Write-Through会非常浪费性能,因为数据可能更改了很多次,却没有被读取,白白的每次都写入缓存造成写入延迟。
- Write-Back
又叫做Write-Behind,和Write-Through写入的时机不同,Write-Back将缓存作为可靠的数据源,每次都只写入缓存,而写入数据库则采用异步的方式,比如当数据要被移除出缓存的时候再存储到数据库或者一段时间之后批量更新数据库。即先更新缓存,缓存定期异步更新数据库。
应用场景:读写效率都非常好,写的时候因为异步存储到数据库,提升了写的效率,适用于读写密集的应用。
优点:写入和读取数据都非常的快,因为都是从缓存中直接读取和写入;对于数据库不可用的情况有一定的容忍度,即使数据库暂时不可用,系统也整体可用,当数据库之后恢复的时候,再将数据写入数据库。
缺点:有数据丢失的风险,如果缓存挂掉而数据没有及时写到数据库中,那么缓存中的有些数据将永久的丢失了。
- Write-Around
和Write-Through不同,更新的时候只写入数据库,不写入缓存,结合Read-Through或者Cache-Aside使用,只在缓存未命中的情况下写缓存。
应用场景:适合于只写入一次而很少被读取的应用。
优点:相比较Write-Through写入的时候的效率较高,如果数据写入后很少被读取,缓存也不会被没用到的数据占满。
缺点:如果数据会写入多次,那么可能存在缓存和数据库不一致
12. 什么是缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
解决:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存;对一定不存在的key进行过滤,可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
13. 什么是缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期,热点数据key失效),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决:设置热点数据永远不过期;分布式锁;异步后台更新,后台任务针对过期的key自动刷新。
14. 什么是缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力,导致系统崩溃。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待;做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期;不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
架构层面解决: 提升系统可用性, 监控、报警完善
15. 连接Redis
# StrictRedis方式
from redis import StrictRedis
redis = StrictRedis(host='localhost', prot=6379, db='test', password='123456')
# 设置一个字符串值
redis.set('name', 'Bob')
print(redis.get('name'))
# 结果
'''
b'Bob'
'''
# ConnectionPool连接方式
from redis import StrictRedis, ConnectionPool
pool = ConnectionPool(host='localhost', port=63799, db='test', password='123456')
redis = StrictRedis(connection_pool=pool)
# ConnectionPool支持通过URL来构建
from redis import StrictRedis, ConnectionPool
url = 'redis://:123456@localhost:6379/test'
pool = ConnectionPool.from_url(url)
redis = StrictRedis(connect_pool=pool)
16. Redis异步队列
一般使用list结构作为队列,rpush生产消息,lpop消费消息,当lpop没有消息的时候,要适当sleep一会再重试。
缺点:在消费者下线的情况,生产者的消息会丢失,得使用专业的消息队列如Rabbitmq。
17. 生产一次消费多次
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
18. Redis分区
分区是分割数据到多个Redis实例的处理过程,因此每个实例只保存key的一个子集。
分区的优势:
- 通过利用多台计算机内存的和值,允许我们构造更大的数据库。
- 通过多核和多台计算机,允许我们扩展计算能力;通过多台计算机和网络适配器,允许我们扩展网络带宽。
分区的不足:
- 涉及多个key的操作通常是不被支持的,如当两个set映射到不同的redis实例上时,你就不能对这两个set执行交集操作。
- 涉及多个key的redis事务不能使用。
- 当使用分区时,数据处理较为复杂,比如你需要处理多个rdb/aof文件,并且从多个实例和主机备份持久化文件。
- 增加或删除容量也比较复杂。redis集群大多数支持在运行时增加、删除节点的透明数据平衡的能力,但是类似于客户端分区、代理等其他系统则不支持这项特性。
分区类型:
- 范围分区:最简单的分区方式是按范围分区,就是映射一定范围的对象到特定的Redis实例。
- 哈希分区:另外一种分区方法是hash分区。这对任何key都适用,也无需是object_name: 这种形式。
18. mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
相关知识:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略(回收策略)。redis 提供 6种数据淘汰策略:
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
持续跟新,更多Redis数据库基础知识,详见个人博客归纳。
欢迎留言补充,批评指正!