Redis是一种内存型数据库。传统的数据库储存在硬盘中,而Redis数据库存在内存中,所以读写速度非常快。因此redis广泛用于缓存方向,除此之外也经常用于实现分布式锁。redis提供了多种数据类型来支持不同的业务场景。
除此之外,redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。
为什么要用Redis/为什么要用缓存
高性能和高并发
高性能:从内存读取数据比从硬盘读取要快很多。如果数据库中对应的数据改变之后,同步改变缓存中相应的数据即可。
高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以可以考虑将数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
Redis为什么那么快
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换造成的开销
- 采用了非阻塞I/O多路复用机制
Redis常见数据结构
- String
- Hash
- List
- Set
- Sorted Set
Redis内存淘汰机制
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持久化机制 (重要)
持久化:将内存中的数据写入到硬盘里面。主要是为了之后重用数据(比如重启、机器故障之后恢复数据),或者为了防止系统故障而将数据备份到一个远程位置。
Redis
不同于memcache
很重要的一点在于Redis支持持久化,提供了两种不同的持久化方式快照(snapshotting, RDB)
,另一种方式是只追加文件(append-only file, AOF)
。
快照
Redis通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
AOF(append-only file)持久化
默认没有开启,可以通过下面的参数开启:
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 主从架构
单机的redis,能够承载的QPS大概在上万或者几万不等。对于缓存来说,一般都是用来支持读高并发的。因此Redis架构实现上会采用主从架构(master-slave):一主多从,主节点(master mode)负责写,并且将数据复制到其他从属节点,从节点(slave node)负责读。即所有的读请求全部走从节点,这样可以轻松实现水平扩容。
redis replication
→
ightarrow
→ 主从架构
→
ightarrow
→ 读写分离
→
ightarrow
→ 水平扩容支撑高并发
Redis Replication的核心机制
- redis采用异步方式复制数据到slave节点。(redis2.8开始,slave node会周期性地确认自己每次复制的数据量)
- 一个master node配置多个slave节点
- slave node也可以连接其他slave node
- slave node在做复制的时候,不会block master node的工作
- slave node在做复制的时候,也不会block对自己的查询操作,它会使用旧的数据集来提供服务;但在复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务。
- slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量。
如果采用了主从架构,那么建议必须开启master node的持久化机制,不建议使用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后一经复制,slave node的数据也丢失了。
另外,master的各种备份方法,也需要做。万一本地的所有文件丢失了,从备份中挑选一份rdb去恢复master,这样才能保证重新启动的时候,是有数据的。slave node可以自动接管master node,但又有可能sentinel还没有检测到master failure,master node就自动重启了,仍有可能导致上述slave node的数据被清空。
master的持久化和多种备份方案都是为了防止重启是数据不为空从而导致slave结点数据清空。
Redis主从复制核心原理
启动一个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.主从复制的断点续传。
mastet 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事务
Redis通过MULTI
、EXEC
、WATCH
等命令来实现事务(transaction)功能。事务提供了一种将多个请求打包,然后一次性的,按顺序的执行多个命令的机制,并且在事务执行期间,服务不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕。
Redis中,事务总是具有原子性(Atomicity)
、一致性(Consistency)
、隔离性(Isolation)
,并且当Redis运行在某种特定的持久化模式下时,事务也具有持久性(Durability)
。
缓存雪崩和缓存穿透
缓存雪崩
缓存同一时间大面积的失效,所以,后面的请求全都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方法:
- 事前:尽量保证redis集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
- 事中:本地ehcache缓存+hystrix限流&降级,避免MySQL崩掉。
- 事后:利用redis持久化机制保存的数据尽快恢复缓存。
缓存穿透
大量请求的key根本不在缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
解决方法:
-
缓存无效key:如果缓存和数据库都查不到某个key就写一个到redis中并设置过期时间。并不能从根本上解决问题,尽量要将无效的key的过期时间设置短一点。
-
布隆过滤器:把所有可能请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来请求的值是否存在于布隆过滤器中,存在的话才会走正常流程,不存在的话直接返回请求错误信息。
如何解决Redis并发竞争key问题
并发竞争key的问题指的是多个系统同时对一个key进行操作,但最后执行的顺序与我们期望的不同,这样导致了结果的不同。
分布式锁(zookeeper和redis都可以实现分布式锁)
基于zookeeper临时有序节点可以实现分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点序号中的最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
如何保证缓存与数据库双写时的数据一致性
读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
最好不要使用这个方案,串行之后系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
参考
来自Java Guide面试突击版,百度可得最新版本,这里有删减和修正以及扩充。