redis为什么快
因为redis是基于内存操作。并且6.0版本之前的redis是单线程的,可以省去很多线程间的切换时间,避免多线程对性能上的消耗。redis的IO多路复用处理并发类问题:redis利用epoll来实现多路复用,先将链接信息和事件放到队列中,然后依次放到文件事件分配器中,然后事件分配器再分给事件处理器。
redisson分布式锁实现原理
使用redisson加锁,三步:1、调用redisson.getLock(lockKey)获取你自己定义的这个lockKey的锁对象。2、加锁:redisson.lock(),在try中。3、redisson.unlock()释放锁。在finally中。
业务流程:redisson底层是用setNx命令,核心加锁代码是tryLockInnerAsync方法,是用lua脚本写的。if逻辑中,先通过exists命令判断这个key存不存在,不存在就把它hset进去,设置key,value和超时时间,默认是30s(可以通过配置修改),lua脚本在redis中执行相当于一条命令,所以具有原子性;key存在则返回循环等待。并且在执行过程中有一段定时线程任务以递归的方式保证锁续命。
主从架构中如果要保证可用性的话可以采用redlock实现。redlock底层实现类似于zookeeper,加锁时要同时对多个节点保证加锁成功,并且还不是很成熟,性能也差不推荐使用。
并且高并发下,redis可以以分段加锁的方式保证性能上的提升(类似于hashmap)。
分布式锁还可以解决缓存数据库双写不一致问题。
redis的5中类型及其原理
String
String类型底层又分为:int、raw(大字符串长度大于44)、embstr(小字符串长度小于44),可以用命令encoding key 查看,且int类型的可以用incr和decr原子递增和递减。至于其他命令,可以参考redis官方命令规范。String类型应用场景:常规的单值缓存或者对象缓存都可以用String,基于get、set(key,value)是比较简单且常用的缓存类型。并且SETNX可以实现分布式锁。
Hash
hash常用操作为:hset key field value;hget/hdel key field等。底层数据结构为ziplist和hashtable(当hash对象保存的键值对数量少于512或键值的字符串长度都小于64时是ziplist类型)。hash类型的优点:比String更节省空间,同类数据归类整合存储,更加便于数据管理。缺点:过期功能不能使用在field上,只能在key上;并且hash类型在集群架构下不适用。应用场景:电商购物车。
List
list的结构为LPUSH/RPUSH(从左/右一次存入),LPUP/RPUP(从左/右依次取出),LRANGE msg m n(从左取出m-n范围的数据)等。底层数据结构:压缩列表ziplist和双向链表linkedList(跟hash一样,当满足一定条件时ziplist会转化为LinkedList)。应用场景:微信和公众号消息流,因为list存入是依次存入的,所以取得时候也是依次有序取出(可以很好的用来排序)。
set
set底层数据结构为intsett数组和hashtable(当对象保存的所有元素都是整数值时或者集合对象总数小于512时是intset类型)。set常用操作:SADD key member [member…](往集合key中存入元素 )等。而set有很多取交集、并集、差集等运算(SINTER、SUNION、SDIFF)。利用这些可以把set用于抽奖、朋友圈点赞、关注和一些筛选相关的场景。
zset
zset的底层数据结构为ziplist和skiplist(跳表)。跳表:在链表的基础上每两格往上加一个索引,依次往上加,查询时根据这些个索引再依次往下找。因此,跳表这种结构属于用空间换时间。内存占用多,但速度快。zset常用操作:ZADD key score member[[score member]…](往有序集合里加入带分值的元素)等等,zset命令很多,包括取并集、交集等,具体查我的redis命令文档。应用场景:排行榜等。排行榜:排行榜名称为key、score为点击量(即数量)、member为排名对象(比如书名)。
stream
redis是可以用来做消息队列的,在我们之前kafka资源还没有申请到位的时候用过一段时间redis做消息队列。
XADD - 添加消息到末尾
XTRIM - 对流进行修剪,限制长度
XDEL - 删除消息
XLEN - 获取流包含的元素数量,即消息长度
XRANGE - 获取消息列表,会自动过滤已经删除的消息
XREVRANGE - 反向获取消息列表,ID 从大到小
XREAD - 以阻塞或非阻塞方式获取消息列表
redis做消息队列有两个弊端:消息堆积、消息丢弃。当队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。
因为 Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,依旧可能被强行删除。但 Kafka、RabbitMQ 这类消息队列就不一样了,它们的数据都会存储在磁盘上,磁盘的成本要比内存小得多,当消息积压时,无非就是多占用一些磁盘空间,相比于内存,在面对积压时也会更加轻松。
Redis 相比于 Kafka、RabbitMQ,部署和运维更加轻量。如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。
如果你的业务场景对于数据丢失非常敏感,而且写入量非常大,消息积压时会占用很多的机器资源,那么我建议你使用专业的消息队列中间件。
redis的持久化、主从、哨兵
redis持久化方式
- RDP:可以通过save参数设置,让redis在N秒内数据集有M个键被改动时自动保存数据集。也可以在redis客户端手动执行save或bgsave命令来生成.rdb文件。每次命令执行新的快照rdb文件都会覆盖老的rdb文件。
- AOF:因为rdb快照功能并不是非常耐久,并且如果redis突然故障宕机,那么就会丢失最近写入但仍未保存到快照中的数据。所以redis后来版本就默认了AOF作为持久化方式。AOF持久化方式通过配置参数# appendonly yes开启。有三个选项:1、appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。2、appendfsync everysec:每秒 fsync 一次,足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。(默认)。3、appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。 AOF重写:AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据生成aof文件,写入频率可以通过参数# auto-aof-rewrite-min-size 64mb 和 # auto-aof-rewrite-percentage 100 控制。AOF还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF。
- redis4.0后混合持久化:通过配置参数# aof-use-rdb-preamble yes 开启混合持久化方式。混合持久化,AOF重写时不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换。于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的AOF 全量文件重放,因此重启效率大幅得到提升。
redis主从架构
如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个SYNC命令(redis2.8版本之前的命令)给master请求复制数据。master收到SYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,master和slave断开重连后支持部分复制。
从2.8版本开始,slave与master能够在网络连接断开重连后只进行部分数据复制。master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。关于流程图和配置信息参考我的文档。
redis哨兵:
sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。 哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过 sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis 主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)。具体配置参数及spring下的哨兵配置参考文件。
redis高可用集群 redis Cluster
如果突然并发量特别大或者系统对redis依赖过高时,如果是哨兵主从架构的话,我们知道在主节点挂掉从节点选举master时也会有几秒甚至十几秒的宕机时间,并且在高并发下单个哨兵主从的可用性是很低的。对此,就需要我们的redis高可用集群了。集群跟哨兵一样可以通过添加配置文件参数进行添加即可。
redis集群需要至少要三个master节点,加上如果是一主一从的话,一共需要最少6个节点。并且主节点一般是奇数最好(主要是半数以上的选举机制,所以出于节省机器,3台和4台基本没很大区别,而4台与5台有差距很多,以此类推)。
Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。槽位定位算法Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。HASH_SLOT = CRC16(key) mod 16384。
redis Cluter集群间的通讯机制
redis Cluter节点间采用gossip协议进行通讯。gossip包含ping、pong、fail、meet等多种消息。gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。
redis选举原理
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:1.slave发现自己的master变为FAIL2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息3.其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack4.尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
5.slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)6.广播Pong消息通知其他集群节点。
从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票•延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms•SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。
哨兵leader选举流程
当一个master服务器被某sentinel视为客观下线状态后,该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。每个发现master服务器进入客观下线的sentinel都可以要求其他sentinel选自己为sentinel的leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel的leader。如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移操作,从存活的slave中选举出新的master,这个选举过程跟集群的master选举很类似。哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,可以正常选举新master。不过为了高可用一般都推荐至少部署三个哨兵节点。为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似。
redis缓存设计与性能优化
缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
解决方案:
- 把空对象加个时间缓存起来。
- 多个IP或空对象高并发访问时可以使用布隆过滤器。布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。底层运用了大量的数学及概率计算。
布隆过滤器
实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
Bloom Filter 原理: 底层是一系列算法:
创建一个m位BitSet,先将所有位初始化为0,在add的时候会计算对应的hash,将这个数组的不同的数对应的hash值分布在不同的位上,有数据的位值设为1。然后下次在查询的时候会根据该hash值算出对应的位上位1的话说明有值,反之没有。但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)。所以这个也是他的一个误识别率。对于误识别率,Bloom Filter 有提供一个参数是可以手动设置误识别率大小的,其原理其实就是通过改变布隆过滤器长度、已添加的元素数量和hash的次数来改变的(过滤器长度增加和hash次数增加可以减小误识别率,但对应的性能也会下降),并且这三者直接是有一个计算公式在的。
缓存失效
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。
缓存雪崩
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会达到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。预防和解决缓存雪崩问题, 可以从以下三个方面进行着手。1) 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。2) 依赖隔离组件为后端限流并降级。比如使用Hystrix限流降级组件。3) 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
热点缓存key重建优化
开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。要解决这个问题主要就是要避免大量线程同时重建缓存。我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。