Redis是一种内存型数据库。传统的数据库储存在硬盘中,而Redis数据库存在内存中,所以读写速度非常快。因此redis广泛用于缓存方向,除此之外也经常用于实现分布式锁。redis提供了多种数据类型来支持不同的业务场景。
除此之外,redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。
高性能和高并发
高性能:从内存读取数据比从硬盘读取要快很多。如果数据库中对应的数据改变之后,同步改变缓存中相应的数据即可。
高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以可以考虑将数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
对Redis来说,所有的key都是String类型。
incr article:aricleId:clickTimes
)incr user:id:generate
)社区应用(微博、朋友圈、博客、论坛)
创建一条微博内容set user:1:post:91 "hello world"
lpush post:91:good "someone"
、多少人点赞llen post:91:good
、哪些人赞lrange post:91:good 0 -1
)lpush post:91:reply "enen"
、查询回复lrange post:91:reply 0 -1
)代替String,以更合理的方式保存对象。
存储、读取、修改用户属性
Redis Stream是5.0版本新增的数据结构,主要用于消息队列(MQ, Message Queue)。Redis本身具有发布订阅(pub/sub)机制来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开,Redis宕机等,消息就会被丢去。
即发布订阅(pub/sub)可以分发消息,但无法记录历史消息。
Stream提供了消息的持久化和主备复制功能,可以让客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证信息不会丢失。
Redis Stream采用的数据结构是链表,结构如下,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对用的内容:
每个Stream都有一个唯一的名称,即为Redis的key,在首次使用xadd
指令追加消息时自动创建。
Consumer Group
:消费组,使用XGROUP CREATE
命令创建,一个消费组有多个消费者。last_delivered_id
:游标,每个消费者组会有一个游标last_delivered_id
,任意消费者读取了消息都会使游标向前移动。pending_ids
:消费者的状态变量,作用是维护消费者未确认的id。pending_ids
记录了当前已经被客户端读取的消息,但是还没有ack。Redis采用的是定期清除+惰性删除的策略。
定时删除,需要用一个定时器来负责监视key,过期自动和删除。虽然内存可以及时释放,但是这十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求上,而不是删除key上。
定期删除
,redis默认每隔100ms检查一下,是否有过期的key,有过期的key则删除。这里需要强调一下的是,redis并不会检查所有的key,而是会随机抽取。如果只采用定期删除策略,会导致很多key到时间而没有删除,于是就需要惰性删除。惰性删除
,并不是直接删除,而是你在获取某个key的时候,redis会检查一下是否过期,过期了才删除。
如果定期删除没有删除key,而且你也没有即时去请求key,惰性删除没有起效果。Redis的内存会越来越高,这时候就需要采用一些内存淘汰机制。
redis提供6种数据淘汰策略:
volatile-lru
:从设置的过期时间数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。volatile-ttl
:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。volatile-random
:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。allkeys-random
:从数据集(server.db[i].dict)中任意选择数据淘汰。no-eviction
:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。4.0版本后增加以下两种:
volatile-lfu
:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。allkeys-lfu
:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用key。总结:两种操作对象:已设置过期时间的数据集(server.db[i].expires)
和数据集(server.db[i].dict)
,四种机制lru
、lfu
、random
、ttl
(仅针对过期时间数据集)加no-eviction
。
持久化:将内存中的数据写入到硬盘里面。主要是为了之后重用数据(比如重启、机器故障之后恢复数据),或者为了防止系统故障而将数据备份到一个远程位置。
Redis
不同于memcache
很重要的一点在于Redis支持持久化,提供了两种不同的持久化方式快照(snapshotting, RDB)
,另一种方式是只追加文件(append-only file, AOF)
。
Redis通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照有两种实现方式,对应两个Redis命令:
save
阻塞主进程,客户端无法连接Redis,等SAVE命令完成后,主进程才开始工作,客户端才可以连接。bgsave
bg代表backgroud的意思。fork
一个save的子进程,执行save过程中,不影响主进程,客户端可以正常连接Redis。等子进程fork执行完save操作后,通知主进程,子进程关闭。Save命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以RDB 文件的形式保存到硬盘。
BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。
bgsave
详细过程如下:
fork()
函数复制一份当前进程(父进程)的副本(子进程)因为主进程还在提供服务,内存中的数据很有可能被更改,这时候你的子进程备份的数据有可能是错误的。
Copy On Write(写时复制,COW)
执行
BGSAVE
命令或者BGREWRITEAOF
命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高负载因子的阈值,从而避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存。-《Redis设计与实现》
基本原理简述如下: fork()执行之后,kernel将父进程中所有的内存页的权限都设置为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,彼此相安无事。但当某个进程写内存时,cpu检测到内存页是read-only的,于是出发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
优点:
缺点: 如果fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的页异常中断(page-fault),得不偿失。
默认没有开启,可以通过下面的参数开启:
appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令。Redis就会将该命令写入硬盘中那个的AOF文件。AOF文件和RDB文件位置相同,可以通过dir参数设置。
appendfsync always # 每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec # 每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no # 让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑appendfsync everysec
选项,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只丢失一秒之内产生的数据。当硬盘忙于写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
Redis 4.0
开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项aof-use-rdb-preamble
开启)
开启混合持久化之后,AOF重写的时候就直接把RDB的内容写到AOF文件开头,这样做的好处是可以结合RDB和AOF的优点,快速加载的同时避免丢失过多的数据,当然缺点也是有的,AOF里面RDB部分的压缩格式不是AOF格式,可读性较差。
单机的redis,能够承载的QPS大概在上万或者几万不等。对于缓存来说,一般都是用来支持读高并发的。因此Redis架构实现上会采用主从架构(master-slave):一主多从,主节点(master mode)负责写,并且将数据复制到其他从属节点,从节点(slave node)负责读。即所有的读请求全部走从节点,这样可以轻松实现水平扩容。
redis replication → \rightarrow→ 主从架构 → \rightarrow→ 读写分离 → \rightarrow→ 水平扩容支撑高并发
如果采用了主从架构,那么建议必须开启master node的持久化机制,不建议使用slave node作为master node的数据热备。如果你关掉master node的持久化,可能在master宕机重启的时候数据是空的,然后一经复制,slave node的数据也丢失了。
另外,master的各种备份方法,也需要做。万一本地的所有文件丢失了,从备份中挑选一份rdb去恢复master,这样才能保证重新启动的时候,是有数据的。slave node可以自动接管master node,但又有可能sentinel还没有检测到master failure,master node就自动重启了,仍有可能导致上述slave node的数据被清空。 master的持久化和多种备份方案都是为了防止重启是数据不为空从而导致slave结点数据清空。
启动一个slave node时,它会发送一个PSYNC
命令给master node。如果slave node和master node是初次连接,则会触发一次full resynchronization
全量复制。此时master会启动一个子进程,开始生成一份RDB快照文件,同时还将从客户端client新收到的所有写命令缓存在内存中。RDB文件生成完毕后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后从本地磁盘加载到内存中。接着master会将内存中缓存的写命令发送到slave,slave也会同步这些数据。slave如果跟master之间发生了网络故障,断开了连接,会自动重连,连接之后master node仅会复制给slave部分缺少的数据。
1.主从复制的断点续传。
master node和slave node会在内存中维护一个backlog
,同时在backlog
中保存一个replica offset
和master run id
。如果master和slave之间的网络连接断掉了,slave会让master从上次replica offset
开始复制,如果没有找到对应的offset,那么就会执行一次resynchronization
。
master run id
的作用:
根据host+ip来定位master,是不太靠谱的。因为如果master node重启或者数据发生了变化,那么slave node根据run id
仍能做出正确区分。
2.无磁盘化复制
master在内存中直接创建RDB,然后发送给slave,不会在自己本地落地磁盘。只需要在配置文件中开启repl-diskless-sync yes
即可。
3.过期key处理 slave不会过期key,只会等待master过期key。如果一个master过期了key,那么会模拟一条del命令发送给slave。
Redis通过MULTI
、EXEC
、WATCH
等命令来实现事务(transaction)功能。事务提供了一种将多个请求打包,然后一次性的,按顺序的执行多个命令的机制,并且在事务执行期间,服务不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕。
Redis中,事务总是具有原子性(Atomicity)
、一致性(Consistency)
、隔离性(Isolation)
,并且当Redis运行在某种特定的持久化模式下时,事务也具有持久性(Durability)
。
缓存同一时间大面积的失效,所以,后面的请求全都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
如果缓存雪崩是由于设置了相同的过期时间而导致缓存在某一时刻同时失效:
大量请求的key根本不在缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
解决方法:
对于一些设置了过期时间的key,如果其中某一key在某一个时间点过期,恰好有大量的针对这个key的并发请求过来,有可能会压垮后端DB。
解决办法:
缓存预热就是系统上线后,将相关缓存数据直接加载到缓存系统。这样可以避免在用户请求时,先查询数据库,然后再将数据缓存。用户可以直接查询事先被预热的数据。
当访问量剧增、服务出现问题(如响应时间慢或者不响应)或非核心服务影响到核心服务的流程时,仍然需要保证服务是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以通过配置开关来实现人工降级。
降级的最终目的是保证核心服务是可用的,即使有损。但有些服务是无法降级的,比如加入购物车、结算等。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采用服务降级策略,例如一个比较常见的做法就是。Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
并发竞争key的问题指的是多个系统同时对一个key进行操作,但最后执行的顺序与我们期望的不同,这样导致了结果的不同。
分布式锁(zookeeper和redis都可以实现分布式锁)
基于zookeeper临时有序节点可以实现分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点序号中的最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
最好不要使用这个方案,串行之后系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
来自Java Guide面试突击版,百度可得最新版本,这里有删减和修正以及扩充。