keys命令和scan命令
keys:全量遍历,用于找出符合特定正则规则的key。当存储的是一个bigkey时,性能比较差,会引起阻塞,尽量避免使用;
scan:相比于 keys 来说对于遍历key更加友好,采用的是一种渐进式的遍历方式,完整命令:
SCAN cursor [MATCH pattern] [COUNT count]
cursor:哈希桶的索引值;
pattern:正则表达式
count:一次遍历的key的数量,这个值仅能作为参考,实际底层不一定会以这个值进行遍历。
第一次遍历时,cursor为0,然后遍历完成后返回下一次遍历需要传的cursor的值,一直到最后返回0代表遍历结束。
主从工作原理
当slave连接上了master并建立起了一个socket长连接,无论salve是否已经连接过master,都会发送一个psync命令给master。
master收到命令的时候会生成当前数据的rdb快照文件,然后将快照文件发送给salve。
salve收到master的rdb文件后会情况当前记录的数据集,然后开始从快照文件中同步数据。
master在发送快照文件之后会继续接收客户端的指令,这时候会将修改数据集的指令缓存到内存中,当salve同步完后会将这部分的指令通过socket长连接陆续发送给slave。
当master和slave因为某些原因断开时,salve会自动重连到master。master同时接收多个salve的连接请求时,也只会生成一份rdb快照文件。
数据断点续传
在redis 2.8之后,如果salve和master之前进行数据同步时连接突然断开,在下次连接时可以不用全部复制,可以进行部分数据复制,也就是断点续传。
slave和master在各自的内存中红都维护了 offset(偏移量)和 master的id。如果断开重连后,slave会发送带有offset参数的psync命令,告诉master需要从offset开始的地方进行未完成数据的复制。但是需要注意的是,如果offset已经不在master记录里,或者master的id已经改变,记录的id和现有id对不上,就会进行数据的全量复制。
redis集群方案
哨兵模式
哨兵模式下,会启动sentinel的redis服务,这种服务并不提供读写功能,只用来监控redis的实例节点。
当client第一次连接redis时,会从sentinel获取到master的信息,后续的指令直接发送到master,不经过sentinel。
当master宕机时,会经历如下步骤:
- 发现master宕机的sentinel节点A发起投票,投票内容是是否认为master宕机,过半数的sentinel节点同意才可将master下线;
- 发现master宕机的sentinel节点A再发起投票,投票内容是要求其他sentinel将自己选为leader,一个sentinel在一轮投票中只会投一次票;
- 如果超过半数节点同意,那么sentinel节点A就会成为leader,如果这一轮平票,那么每次参选的节点会休息一个随机的时间,再发起下一轮投票,知道选出一个leader为止;
- leader选出后,会进行故障转移,从slave节点选出一台做为master节点,matser节点恢复后会成为slave节点继续运行。
优点:
- 主从备份;
- 具备一定的容错与恢复功能,即使matser宕机,也能保证系统运行。
缺点:
- 无法在线扩容,容量限制与单机的配置;
- 需要额外的资源启动 sentinel,且slave节点不提供服务。
集群模式
集群模式具备以下特点:
- redis会将数据划分为16384 个 slots(槽位),每个节点只负责存储一部分的slots,slots的信息存储在每个节点中;
- client本地也缓存了一份slots信息,client可以连接到任何一个节点当中,当client发起指令时,会对key进行CRC16算法得到一个整型值,然后用整型值对16384 进行取余,这样就可以得到这个key的slots;
- 如果client想要操作的key的slots不在client连接的节点负责的slots中,该节点会发送一个带有目标节点的信息的命令给client,client收到信息后会更新本地的slots,并且跳转到目标节点上。
- 每个节点除了开放提供服务的端口号,还会开放一个用于集群中进行通信的端口号,这个值默认是己提供服务的端口号+10000;
- 每个节点都至少有一个slave节点,并且由于内部的投票半数原则,所以构建一个集群需要6个节点(3主3从);
如果有一个master宕机,会经历以下恢复步骤:
- slave节点发现自己的master宕机了,会向其他节点的master发送FAILOVER_AUTH_REQUEST,请求其他节点进行失败确认,并将集群中的currentEpoch +1;
- 其他节点的matser收到FAILOVER_AUTH_REQUEST,验证发送者的合法性后,会发送FAILOVER_AUTH_ACK信号,对于一个epoch只会响应一次;
- 发起失败确认的slave收集所有的FAILOVER_AUTH_ACK信号后,如果收到的信号超过集群中半数master以上的ack,那么salve节点就会成为master节点,并给其他master节点发送pong信号告知。
- 如果选举失败,说明不止一个salve节点在进行选举,每个slave节点在进行下一次投票选举之前都会延迟一段时间。
延迟时间公式:
DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
SLAVE_RANK 表示已经从master同步数据数量的rank,rank越低,代表同步的数据越新。
currentEpoch 作用:
当集群状态改变,该值就会增加,这是为了保证,每个matse节点对于每一次投票对只有一次响应的结果。
salve并不是matser一宕机就马上进行选举,而是会延迟一段时间进行选举,这是为了保证master的宕机状态能在集群中广播。
缓存穿透
缓存穿透是值客户端查询一个根本不存在的键,在缓存层找不到数据,然后直接穿透到数据库层进行查询,使得数据库压力增大,一般出现这种问题可能有两个原因:
- 业务设计不规范或数据出现问题
- 恶意攻击。
解决办法:
- 业务层面接收数据时可对请求参数简单做一些校验,如果不符合规则直接拒绝;
- 查询不到数据时,可对该key缓存一个空对象,并设置过期时间,保证短时间内请求不会落到数据库层;
- 使用布隆过滤器,每次进行查询时,先询问布隆过滤器,布隆过滤器回答不存在,则该key一定不存在;布隆过滤器回答存在,该key不一定存在。
缓存击穿
缓存击穿指的是在高并发场景下,某一批热点数据在同一时刻缓存集体失效,导致请求在短时间内落到数据库层导致压力突然增大,甚至宕机。
解决办法:
- 增加一批缓存数据时,不要设置一样的过期时间,而是随机设置同一时间段内不同的过期时间;
- 对于热点key,每次访问都获取一把分布式锁,不过这样会对锁的访问压力较大。
缓存雪崩
缓存雪崩指的是打在缓存层上的请求过多,超过了缓存层能承受住的压力,这时就会像雪崩一样,导致数据库压力突然增大。
解决办法:
- 保证缓存层的服务高可用,使用哨兵架构或者集群架构;
- 依赖隔离组件为后端限流并降级,如Sentinel或Hystrix限流降级组件。
缓存与数据库数据不一致
双写不一致
出现原因:更新数据时,会分为更新缓存和更新数据库两步操作,但这两步不是原子操作,就会出现缓存与数据库数据不一致的场景。
线程1写完数据库,在更新缓存之前cpu转而执行后来的线程2,线程2写完数据库后并且更新缓存,回到线程1后更新缓存,就会使线程2的缓存值失效。
删除key模式
如果更新数据时,采用删除key的模式。这样还是会出现不一致的情况。
这里线程1写完数据库后,删除了缓存,然后执行线程3查询缓存层没有,查询到了数据库层,在更新缓存之前线程转而执行了线程2,并写数据和删除缓存后,回到了线程3,这里线程3查到的就是一个旧值,出现了不一致的情况。
延迟双删
延迟双删指的是线程在写完数据库删除缓存后,延迟一段时间再进行删除,这样能有效减少不一致的概率。
这种做法并没有完全解决,还是会出现不一致的情况。并且增加了每次写数据的耗时,使得系统的吞吐量变小 。
不一致的解决方案:
- 对于能容忍一段时间的脏数据情况,并不需要考虑这个问题,只要给每个key都设置一个过时时间就可;
- 加锁,使访问数据串行化:每次写数据时都放进一个队列中,操作数据时从队列取出进行操作;使用读写锁,读读的情况相当于无锁。
总结:
我们使用缓存都是为了解决读多写少的情况,这种情况都是可以容忍一段时间的不一致的;如果场景是写多读少,又要保证数据的强一致,就没必要使用缓存了,这种情况还不如直接访问数据库来的实在,不要为了使用缓存,又做了很多设计保证数据的一致性而导致系统复杂性增加,这样得不偿失。