主从结构
Redis主从结构与MySQL的主从结构类似,常见结构如一主一从,一主多从,树状主从。
- 主从结构主要用于实现读写分离,数据备份,分担服务器负载。
- 缺点在于主库和从库是同一份数据,若从库较多,数据冗余,浪费资源。同时存在主从延迟。并且主库故障时从库不会自动晋升,需要哨兵介入,可能导致数据丢失。
主从复制的工作流程
- 从机启动,发送psync命令给主机请求同步。
- 如果从机第一次连接主机,触发全量同步,主机fork子进程生成RDB快照并发给从机。
- 如果从机是重新连接主机,触发断点续传,从机在同步请求中携带offset字段和主机id,主机判断请求中的主机id是否是自己的id,offset对应的数据是否在自己的缓冲区中,若在则传递offset后的数据给从机,若不在进行全量同步。
- 从机收到数据后,先将数据写入磁盘,清空自己的数据再将其加载到内存,加载完后从机回复确认。
- 主机将缓存的新的写命令发给从机进行同步,之后主机和从机之间会维护一个TCP长连接,主机持续与从机进行同步。
环形缓冲区
- redis使用环形缓冲区保存最新的写命令,主机保存offset记录自己写到的位置,从机保存offset记录自己读到的位置。 主机判断从机要读取的字段是否还在环形缓冲区中,如果在就断点续传,如果不在就全量同步。
- 如果主机缓存了太多命令或者主机写入速度远超从机同步速度时,环形缓冲区会存满,此时新的命令会覆盖最旧的命令(当然主机也可以不继续接受新命令,等待缓冲区清空),如果旧命令被覆盖,未读到这些旧命令的从机后续同步时需要执行全量同步,所以要尽可能增大环形缓冲区。
主从延迟
- 主从延迟导致读取到脏数据/过期数据:
- 若某个从机与主机的延迟超过阈值,则客户端不从这个从机读取数据了,这样可以减少读取到脏数据。
- 默认从库不会删除过期数据,但是可以让从库读取时检查数据是否过期,若过期则返回null。
- 定期使用scan命令扫主库,扫到的过期数据会被淘汰并让从机删除对应数据,但是扫描命令会对redis造成一定压力。
- 主从延迟导致数据丢失:
- 异步复制:master到slave的复制是异步的,部分数据还没复制到slave,master就宕机了,此时这部分数据就丢失了。
- 解决方案1:设置参数,如果所有从机与主机之间的延迟都超过阈值,主机就不再接受新的写请求。客户端可以暂时将数据写入本地缓存或写入消息队列,等主机恢复后再去重新执行。
- 解决方案2. 从机收到数据并持久化后回复ACK,主机才认为数据已同步。
- 脑裂:主从节点网络断了,但是主机与客户端的网络正常,从机中选出新主机,但客户端继续向旧主机发送数据,旧主机把命令缓存到自己的缓冲区(没法同步给其他机器),旧主机重新连回网络被设置为从机,与新主机做全量同步,其缓存数据被清空。
- 解决方案:设置参数,主库至少要能连接到n个从库,且主从延迟不能超过阈值,否则主库不向客户端提供服务。
- 异步复制:master到slave的复制是异步的,部分数据还没复制到slave,master就宕机了,此时这部分数据就丢失了。
哨兵
主从结构在主节点宕机时不会自动将其他从机节点升级为主节点,故需要哨兵来监督各节点的状态并推动主节点的选举。Redis哨兵集群发现有主节点宕机后,会自动选举出新的主节点,将其他从节点指向新的主节点,并通知客户端。
- 注意:哨兵必须要2n+1个,选举出领导者哨兵或主机客观下线都需要n+1个哨兵同意,即必须要过半。
- 注意:哨兵实际是节点上的一个进程,节点宕机哨兵也会宕机,哨兵只负责主机断线时切换新的主机,但在切换期间也可能丢失数据,并不能确保数据不丢失。
哨兵运行流程(选举原理)
- 主观下线:某哨兵检测到主机下线了(发送心跳没回复)。
- 客观下线:过半哨兵认为主机主观下线,则认为主机客观下线。
- 主机客观下线后,哨兵们根据领导者选举算法选举出一个领导者哨兵,并由该哨兵进行故障迁移(领导者选举算法是分布式系统一致性算法Raft的一部分)。
- 领导者哨兵推动选举出新主机:某个从机被选为新主机,哨兵将其设置为独立节点,并修改其他从机节点指向当前主机节点,若掉线的主机重新上线,会将其设为当前主机的从机。
- 另外,从机被选为主机的标准依次是从机与主机断开连接的时长(筛选掉网络差的从机)、优先级、复制进度(offset),Id号。
集群
Redis cluster最小配置6个节点(3主3从,部署在3台物理机器上),集群模式下数据存储在不同的主节点上(数据分片),自带哨兵的故障转移机制,用户的请求可以并行处理。
分区机制/数据映射机制
- Redis Cluster采用哈希槽分区,所有键根据哈希函数映射到16384个槽位上,每个主节点维护一部分槽位。
- 哈希槽实际上就是一个数组,数组[0,2^14 -1]形成hash slot空间。即在数据和节点之间加了一层,槽数是固定的,故数据只需要映射到槽上即可。而槽对于节点来说相当于增大了粒度,便于数据移动。一个集群只能有16384个槽,这些槽会分配给集群中的所有主节点(分片),分配策略没有要求。而当需要存放数据时,先用CRC16算法对key求哈希值,然后对16384取模,余数是几key就落入对应的槽里。
- 最大优势是方便扩缩容和数据查找。将哈希槽从一个节点移到另一个节点并不会停止服务,所以无论添加删除或改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
Redis集群中某个master节点宕机会怎么样?
- Redis集群中某个master节点宕机时,会在其slave中选择一个数据最完整的作为新的master。如果宕机的master无slave,为了保证集群的完整性,整个集群将不可用,当然也可以设置为正常提供服务。
客户端如何访问redis集群的数据?
- 客户端会连接到集群的某个节点,发送查询key的请求。
- 节点先根据key找到对应的哈希槽,如果哈希槽是自己负责的,就直接返回结果,如果由其他节点负责,会返回MOVED永久重定向,告知客户端应该给哪个节点发送请求,客户端再向目标节点发送请求并更新自己缓存的哈希槽分配信息。(注意,这里客户端找错节点可能是由于集群节点扩容缩容导致哈希槽重新分配)。
哈希槽的个数是16384个,即2的14次方,为什么?
- 不选择65535是为了避免哈希槽太大导致心跳包内容太大,消耗过多带宽,并且Redis Cluster的主节点通常不会扩展太多,16384个哈希槽就够用了。
如何重新分配哈希槽?扩容缩容期间可以提供服务吗?
- 一般会自动分配,也可以通过命令手动分配。扩容缩容期间可以提供服务,Redis集群提供了两种重定向机制,分别是ASK临时重定向,MOVED永久重定向。
- 如果请求的key对应的哈希槽还在当前节点(未迁移),则直接响应客户端请求。
- 如果哈希槽在迁移但是key还未迁移走,则直接响应请求。
- 如果哈希槽在迁移但是key已被迁移走,则返回ASK错误告知客户端临时重定向到新节点,客户端收到后向新节点发送ASKING命令,新节点收到后返回确认或者key还没导入完成返回重试错误,客户端发送真正的请求命令,待迁移完成后,原始节点返回MOVED告知客户端永久重定向新节点,客户端更新哈希槽缓存。
- 如果迁移完成,则返回MOVED告知客户端请求新节点。
Redis集群中的节点如何通信?
- 通过Gossip协议,每个节点持有一份元数据,不同节点的元数据变更后不断将其发送给其他节点,保持整个集群所有节点数据完整。这种机制好处在于更新比较分散,有一定延迟,可以降低压力。
- 使用Gossip协议就不需要专门去部署Sentinel集群了,各节点通过Gossip协议互相检测健康状态充当哨兵,在出现故障时可以自动切换。
Redis分布式锁
本地锁只能锁住位于同一个机器(JVM)上的不同线程,分布式锁指对多个JVM的上的线程都可见的锁,对于部署在多个服务器上的微服务起作用。常见的分布式锁实现有zookeeper,redis。
设计分布式锁的要点
- 独占性:任何时刻只能有一个线程有锁,所以获取锁和释放锁时都要保证原子性。
- redis中的setnx就是没有这个值时才可以创建,保证获取锁是原子性操作。
- 使用lua脚本确保解锁是原子性操作。
- 防死锁:不能出现死锁问题,必须有超时重试机制或撤销操作。
- setnx时设置过期时间。
- 不乱抢:每个线程只能解锁自己的锁,不能释放别人的锁。
- setnx的key要带上线程id来区分是否是自己的锁。
- 重入性:同一节点的同一线程获得锁之后,这个线程可以再次获得锁。
- setnx不满足,因为要记录持有锁的线程进入了几次锁,可以使用redis中的hset。
- 超时解锁导致并发:线程A执行时间过长导致锁过期自动释放,此时线程B获取到锁,线程A和B并发执行。
- 可以使用一个守护线程,若执行线程在正常执行,则对锁续期。也可用使用watchdog。
- 高可用:不能因为redis挂了而出现获取锁或释放锁失败的情况,即单点故障问题。但是也不能使用redis集群,因为当主机宕机,从机上位时,这个复制的过程是异步的,很有可能主机已经设置了锁,但是尚未同步到从机时,主机挂了,此时从机上位但是没有锁,线程又在从机这里拿到了锁,出现了多个线程拿着锁的问题。(一句话,主从延迟导致数据丢失)。
- 此时需要使用RedLock。
RedLock
针对单点故障问题,redis官方提供了RedLock算法,用于实现多个实例的分布式锁。不使用主从结构,而使用N个独立master节点(N为奇数),容错数量为N/2,即最多容忍N/2个redis实例失效。 Redisson实现了RedLock。
分布式锁的其他实现
- MySQL实现分布式锁,通过加锁读(for update)来获取锁。
- 优点是实现简单。
- 缺点是没走索引会锁表,MySQL性能差,获取锁失败后需要轮询(消耗资源)。
- redis实现分布式锁,通过setnx指令+lua脚本实现。
- 优点是实现简单,可抗住高并发。
- 缺点是单点故障,不是强一致性的,key过期时间不明确,获取锁失败后需要轮询(消耗资源)。
- Zookeeper实现分布式锁,通过在同一个目录下创建临时顺序节点实现。
- 优点是强一致性,获取锁失败后可以使用监听器监听锁(不用轮询)。
- 缺点是性能不如redis。
- 如何实现?
- 线程通过在zk中创建一个临时顺序节点来获取锁,如果当前线程创建的节点最小则成功获取到锁,如果有比自己小的节点则监听这个节点,线程通过删除节点来释放锁,并通知监听这个节点的线程。zk的临时节点名称不能重复,且会随着客户端的退出而销毁。
- zk是强一致性的(cp),即如果主从数据不一致就不对外提供服务,保证用户读取到的数据始终是一致的,故zk实现的分布式锁可靠性较强 而redis只保证最终一致性,可能有主从延迟导致多个线程获取到锁等问题,可靠性较差。