目录
基础
Redis数据类型
- string
- hash
- list
- set
- sorted set
Redis过期策略
两种过期策略:定期删除+惰性删除
-
定期删除
redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。有两个要点:
- 定期删除指每隔一段时间对数据库做一次检查,删除过期key
- 由于不可能对所有key做轮询删除,所有redis每次随机抽取一些key做检查和删除
-
为什么要随机?
假如redis存了几十万个key,每隔100ms遍历所有设置过期时间的key的话,就会给CPU带来很大的负载。
-
为什么不用单个key的到期删除策略?
这种需要用一个定时器来负责监视key,过期则删除。虽然内存及时释放,但过多的定时器十分消耗CPU资源,得不偿失。
-
惰性删除
惰性删除指我们查询key的时候才对key进行检查,如果过期则删除。显然,它有一个缺点是如果过期的key没有被访问,那么它就一直无法被删除,一直占用内存。
定期删除+惰性删除存在的问题:
如果某个key过期后,定期删除没有删除成功,也没有再次访问key,这时,如果大量这种key堆积在内存中,redis的内存会越来越高,导致redis内存耗尽。此时就应该采用内存淘汰机制。
- noeviction:不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直
接返回错误。Redis 用作缓存时,实际的数据集通常都是大于缓存容量的,总会有新的数据要写入缓
存,这个策略本身不淘汰数据,也就不会腾出新的缓存空间,我们不把它用在 Redis 缓存中。 - volatile-lru:在设置了过期时间的键值对中,移除最近最少使用(最近最久未使用)的键值对。
- volatile-random:在设置了过期时间的键值对中,随机移除某个键值对。
- volatile-ttl:在设置了过期时间的键值对中,移除即将过期的键值对。
- volatile-lfu:在设置了过期时间的键值对中,移除最近最不频繁使用的键值对, 或者移除 最不经常
使用的键值对。 - allkeys-lru:在所有的键值对中,移除最近最少使用(最近最久未使用)的键值对。
- allkeys-random:在所有键值对中,随机移除某些key。
- allkeys-lfu:在所有的键值对中,移除最近最不频繁使用的键值对, 或者移除 最不经常使用的键值对.。
通常情况下推荐优先使用allkeys-lru策列。
Redis为什么快
Redis的速度非常快,单机就可以支撑每秒十几万的并发,相对MySQL来说,性能是MySQL的几十倍。速度快主要有以下几点:
- 完全基于内存操作
- 使用单线程,避免线程切换和竞态产生的消耗
- 基于非阻塞的IO多路复用机制
- C语言实现,优化过的数据结构,基于几种基础的数据结构,大量的优化,性能极高
Redis中只有网络请求模块和数据操作模块是单线程,而其他的如持久化存储模块、集群支撑模块等是多线程的。
Redis的持久化
Redis持久化机制有两种:RDB和AOF
-
RDB
RDB、AOF作为redis持久化机制,用于crash后,redis的数据恢复。这里对这两种机制的原理介绍不做讲解,因为很基础,而且网上对它们的介绍文章数不胜数。这里讲讲它们的优势、劣势和生产中推荐的配置。
优势:
-
只有一个文件dump.rdb,方便持久化
-
RDB相当于内存的一个快照,数据恢复快
劣势:
- bgsave时间长,丢失数据多
-
-
AOF
优势:
- AOF可以配置appendfsync属性为always,每进行一次操作旧记录到AOF文件中一次,数据最多丢失一次
- rewrite机制,缩小文件体积
劣势:
- AOF文件大,通常比RDB文件大很多
- 比RDB持久化启动效率低,数据集大时较为明显
- AOF文件体积可能迅速变大,需要定期执行重写操作来降低文件体积
-
RDB和AOF如何选择
为了达到数据安全性,应该同时使用两种持久化功能。
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。 save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。 save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。 appendonly yes #开启aof持久化 appendfsync always #每次有数据修改发生时都会写入AOF文件。 appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。 appendfsync no #从不同步。高效但是数据不会被持久化。
高级特性
Redis主从复制
Redis可以使用主从同步,从从同步。
第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制目标节点接受完成后将RDB镜像加载到内存。
加载完成后,再通知主节点将期间修改的操作记录同步到复制节点,进行重载就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。所以主从复制需要RDB和AOF两种持久化机制。
-
主从复制实现原理
总的来说主从复制的步骤分为6个步骤:
- 设置主节点的地址和端口
- 建立套接字连接
- 发送PING命令
- 权限验证
- 同步数据
- 命令传播
为了测试,本地机开启两个Redis节点,一主一从,分别监听6379端口(主)和6380端口(从)
主redis可以先启动起来,加入从节点不影响主节点的运行。
-
设置主服务器的地址和端口
在从服务器的配置文件中配置需要同步的主服务器信息,包括IP、端口。
-
配置文件
在从服务器配置文件中加入: 低版本
slaveof masterip masterport
,高版本replicaof masterip masterport
-
启动命令:
redis-server启动命令后加入: 低版本
--slaveof masterip masterport
,高版本--replicaof masterip masterport
-
客户端命令
redis从服务器启动后,直接通过客户端执行命令:低版本
slaveof masterip masterport
,高版本replicaof masterip masterport
上述3种方式是等效的,执行其一即可。具体执行语句是什么,可以参考官网。
-
-
建立套接字连接
执行完
slaveof masterip masterport
,在6380从服务器上执行info replication
命令,查看6380服务器的角色:slave
6379服务器角色:master
-
发送PING命令
建立socket连接之后,redis从节点发送ping命令,检查socket连接是否可用。
-
身份验证
主节点配置了masterauth选项后,从节点需要向主节点进行身份验证。具体做法是,在主节点的配置文件中配置了
masterauth 密码
,从节点的配置文件相应添加masterauth 密码
即可。 -
同步
同步就是将从节点的数据库状态更新成主节点当前的数据库状态。从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。 数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。
-
命令传播
经过上面同步操作,此时主从的数据库状态其实已经一致了,但这种一致的状态的并不是一成不变的。 在完成同步之后,也许主服务器马上就接受到了新的写命令,执行完该命令后,主从的数据库状态又不一致。
数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
另外命令转播我们需要关注两个点: 延迟与不一致 和 心跳机制
主从复制完成,redis主从复制的特点是master负责写操作,从节点负责读。因为从节点配置文件默认配置了replica-read-only yes
导致从节点执行写操作返回错误,读写分离的模式能很大程度提高redis的并发能力。
Redis哨兵模式
Redis主从复制有什么缺陷呢?很明显,当redis master宕机后,整个redis将不可用,需要人工干预来重新启动redis主从模式。在生产上是万万不能真么操作,不符合高可用(HA)特性。而Redis哨兵模式正好可以解决这个问题。
当主节点挂了的时候,sentinel集群通过选举机制在剩下的被监视存活的节点中重新选举master,以保证redis服务对外的高可用。
Redis集群
那么Redis哨兵模式又有什么缺陷呢?试想,当redis保存的数据有几十G上百G时,一台服务器完全放不下时怎么办,很显然,哨兵模式是没办法解决这个问题,哨兵模式下的每台redis服务器都是保存的全量数据,不足以支撑大量数据,而集群分片却能很好解决这一问题。
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
前面说到了主从存在高可用和高扩展的问题,哨兵解决了高可用的问题,而集群就是终极方案,一举解决高可用和高扩展问题。
-
数据分区:
数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。数据分片是高扩展的基础。
-
高可用:
集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
这有一篇redis集群非常详细的讲解认识Redis集群——Redis Cluster - JJian - 博客园 (cnblogs.com)
场景应用
redis分布式锁
setnx+expire
Bitmap
一个经典面试场景,1000W级用户场景的签到优化:
优化1:利用Bitmap实现用户签到存储优化
假如有1000万用户,平均每人每年签到次数为10次,一年下来,则这张表数据量为多少呢?
数据比较吓人: 1亿条
每签到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22 字节的内存,一个月则最多需要600多字节
我们如何能够简化一点呢?
其实可以考虑小时候一个挺常见的方案,就是小时候,咱们准备一张小小的卡片,你只要签到就打上一
个勾,我最后判断你是否签到,其实只需要到小卡片上看一看就知道了。我们可以采用类似这样的方案来实现我们的签到需求。我们按月来统计用户签到信息,签到记录为1,未签到则记录为0。把每一个bit位对应当月的每一天,形成了映射关系。用0和1标示业务状态,这种思路就称为位图(BitMap)。这样我们就用极小的空间,来实现了大量数据的表示。
那么,一个月的签到数据,可以使用一个无符号整数来存储。之前一个月要 600个字节,现在保存一个人一个月的签到数据, 只要 24个字节 ,一下就提升了 25倍。
优化2:利用Redis Bitmap实现用户签到的性能优化:
刚好redis 有Bitmap结构。可以利用Redis Bitmap实现用户签到的性能优化,将当前用户当天签到信息保存到Redis中。
redis的吞吐量为 2Wqps以上, 完全可以满足 1000W用户签到的需求。但是 SpringCloud 微服务够呛,怎么办呢?
优化3:利用Nginx +lus 实现更进一步的用户签到的性能优化:
SpringCloud 微服务够呛,但是nginx可以,nignx的吞吐量为 2Wqps以上, 完全可以满足 1000W用户签到的需求。
缓存雪崩、缓存击穿、缓存穿透
-
缓存雪崩
某⼀时刻发⽣⼤规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是⼤量的请求进来直接打到DB上,db无响应,最后可能导致整个系统的崩溃,称为雪崩。
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了:
- 缓存全盘宕机,缓存挂了
- 大量key在同一时间过期
此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后db无响应,最后导致整个系统的崩溃。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
解决方案:
- 提高缓存可用性:
- 集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
- 多级缓存:设置多级缓存,设置一级缓存本地 guava 缓存,第一级缓存失效的基础上再访问二级缓存 redis,每一级缓存的失效时间都不同。
- 过期时间:
- 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
- 热点数据永不过期。
- 熔断降级:
- 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,可以使用hystrix 类似的熔断,暂时停止业务服务访问db, 或者其他被依赖的服务,避免 MySQL 被打死。
- 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。
-
缓存击穿
一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。具体来是,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方案:
- 预先设置热门数据:在redis高峰访问时期,提前设置热门数据到缓存中,或适当延长缓存中key过期时间。
- 实时调整:实时监控哪些数据热门,实时调整key过期时间。
- 对于热点key设置永不过期。
- 加锁更新:⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。
-
缓存穿透
缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。缓存穿透可能有两种原因:
- 自身业务代码问题
- 恶意攻击,爬虫造成空命中
解决办法:
- 对空值缓存:如果一个查询数据为空(不管数据是否存在),都对该空结果进行缓存,其过期时间会设置非常短。
- 采用布隆过滤器:布隆过滤器可以判断元素是否存在集合中,他的优点是空间效率和查询时间都比一般算法快,缺点是有一定的误识别率和删除困难。
- 设置可以访问名单:使用bitmaps类型定义一个可以访问名单,名单id作为bitmaps的偏移量,每次访问时与bitmaps中的id进行比较,如果访问id不在bitmaps中,则进行拦截,不给其访问。
- 进行实时监控:对于redis缓存中命中率急速下降时,迅速排查访问对象和访问数据,将其设置为黑名单。