Redis 底层存储的还不是一个简单的 linkedlist,而是称之为 快速链表 quicklist 的一个结构
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。 数据量比较多的的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。
Redis 事务只能保证隔离性,不能保证原子性
Redis事务时放到一个事务队列中,由于是单线程的所以保证了隔离性
管道其实是发生在客户端的,将指令顺序颠倒,从而减少网络IO的次数到更高的性能
操作系统回收内存是以页为单位,如果页上有一个key,那么这个页就不会被回收。如果是flushdb,那么所有的key被删除,内存会立即被系统回收。
redis集群模式
1、单机:只有一个redis实例,rw都在一起,不高可用
2、主从
问题一 redis主从同步的方式?
1、全量同步
阶段:在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。
步骤:
1、Slave连接Master服务器,发送SYNC命令。
2、Master接收到命令后,执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令。
3、Master服务器执行玩BGSAVE后,向所有Slave发送发送SNAPSHOT文件,并在发送期间继续使用缓冲区记录此后的所有写命令
4、Slave接收SNAPSHOT后丢弃所有旧数据,载入收到的SNAPSHOT
5、Master发送玩SNAPSHOT后,开始向所有Slave服务器发送缓冲区中的写命令
6、Slave服务器完成SNAPSHOT载入后,接收Master的写命令
2、增量同步
阶段:Master每执行一次Write命令,就会向Slave服务器发送相同的Write命令,Slave接收到命令后执行相应的Write命令。
问题二 同步策略?
1、Master 和 Slave刚连接的时候,进行全量同步
2、全量同步后,进行增量同步
3、Slave可以在任何时候发送全量同步到Master
4、Redis首先尝试进行增量同步,如果不成功,进行全量同步
注意:如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。
3、哨兵
问题一 什么是哨兵?
Redis的哨兵(sentinel) 系统用于管理多个 Redis 服务器,该系统执行以下三个任务:
1、监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。
2、提醒(Notification):当被监控的某个Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
3、自动故障迁移:当检查到Master异常,会自动将其中一个Slave升级为新的Master,并让其他Slave复制新的Master,当Client试图连接失效的Master,集群也会向Client返回最新的Master地址,使得集群可以使用Master代替失效的Master
4、哨兵是节点中的某一个,也是一个分布式系统,哨兵进程通过流言协议(gossipprotocols)接收关于Master是否下线的信息,使用投票协议(agreement protocols)来决定是否执行故障迁移
5、每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown).
6、若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置.
7、虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel).
哨兵(sentinel) 的一些设计思路和zookeeper非常类似
问题三 如何修改哨兵?
步骤:
1、
cp sentinel.conf /usr/local/redis/etc
2、监听Master地址,配置Master授权
sentinel monitor mymaster 192.168.110.133 6379 1 #主节点 名称 IP 端口号 选举次数 sentinel auth-pass mymaster 123456
3、心跳检查间隔时间
sentinel down-after-milliseconds mymaster 5000
4、合格节点
sentinel parallel-syncs mymaster 2
5、启动
./redis-server /usr/local/redis/etc/sentinel.conf --sentinel &
6、停止哨兵模式
4、集群 : redhot
Redis投票选举
步骤:
1、故障节点主观下线
哨兵节点会定时向Redis所有节点发送心跳包检查是否正常,如果在指定时间内未响应,则该哨兵主观认为该节点下线
2、故障节点客观下线
某一个哨兵主观下线不能认为有问题的节点真的下线了,需要询问其他哨兵,如果主观认为下线的哨兵数量大于配置的quorum数量,则认为节点下线。
如果下线的是Slave则没有后续,如果是Master则进行故障转移操作。
3、哨兵集群选举 哨兵Leader
原因:领导者选举,因为只能有一个哨兵执行故障转移动作。
选举:
1、每个哨兵都想其他哨兵发送命令,要求将自己选为Leader。
2、收到命令的哨兵如果没有同意其他哨兵发送的命令,视为拒绝。
3、当哨兵发现自己的票数已经大于总哨兵数量的一半,则任命自己为Leader。
问题一 quorum是干什么的?
用来标示故障转移
假设有5个哨兵,quorum设置为2,那么如果5个哨兵中,有2个都认为master挂掉了,两个哨兵就会进行选举,选举出一个Leader进行故障转移。
集群至少3个节点
4、哨兵Leader选举新Master
Redis的并发竞争问题
场景:并发多线程对同一个key进行写操作
举例:
两个连接同时对price进行写操作,同时加10,最终结果我们知道,应该为30才是正确。
考虑到一种情况:
T1时刻,连接1将price读出,目标设置的数据为10+10 = 20。
T2时刻,连接2也将数据读出,也是为10,目标设置为20。
T3时刻,连接1将price设置为20。
T4时刻,连接2也将price设置为20,则最终结果是一个错误值20。
解决方案:
1、
incrby price 10 //将key为price的加10,
2、可以使用独占锁的方式,类似操作系统的mutex机制。
3、乐观锁的方式进行解决
watch这里表示监控该key值,后面的事务是有条件的执行,如果从watch的exec语句执行时,watch的key对应的value值被修改了,则事务不会执行。
4、使用setnx,内置锁
watch price get price $price $price = $price + 10 multi set price $price exec
redis作为缓存的优势
1、多种类型,String,hash,list,set,zset,Bitmaps,HyperLogLog
2、键过期,发布订阅,事务,Lua脚本,哨兵,Cluster等功能
3、单线程,单线程避免了线程切换以及加锁释放锁带来的消耗
4、非阻塞多路I/O复用机制,使用epoll作为I/O多路复用技术的实现
缓存雪崩
因为redis宕机,导致瞬时大流量打到db,导致db无法支撑。
解决方案:
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + 限流&降级,避免 MySQL 被打死。
- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
缓存穿透
批量的大部分请求key在redis不存在,导致请求打到db中。
解决方案:
每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
缓存击穿
某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
解决方案:
1、若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
2、若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
3、若缓存的数据更新频繁或者缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
普通hash
将一系列在形式上具有相似性质的数据,打散成随机的、均匀分布的数据。
eg:Group = Key % N
一致性hash
一致性Hash通过构建环状的Hash空间代替线性Hash空间的方法解决了普通hash所带来的问题
特点:
首位相连的环形链
执行过程
1、第一次:每个节点(集群)计算Hash,然后记录它们的Hash值,这就是它们在环上的位置。
2、第二次:每个Key计算Hash,然后沿着顺时针的方向找到环上的第一个节点,就是该Key储存对应的集群。
优势
1、集群节点增加:节点增加位置到逆时针第一个节点之间的key被落到新加入节点上。
2、集群节点删除:其余节点在环上的映射不会发生改变,只是原来打在对应节点上的Key现在会转移到顺时针方向的下一个节点上去。
3、节点非增减而是变动:带来的影响还是挺大的。
图例如下
负载均衡系统使用一致性hash带来的问题
1、数据倾斜。
2、缓存雪崩:当环上某一个节点退出,那么该节点受理的请求将全部分发到它顺时针方向的下一个节点,会导致下一个节点压力瞬时增大。
解决方式:
1、引入虚拟节点,扩展整个环上的节点数量。
一个实际节点将会映射多个虚拟节点,这样Hash环上的空间分割就会变得均匀。
引入虚拟节点还会使得节点在Hash环上的顺序随机化,这意味着当一个真实节点失效退出后,它原来所承载的压力将会均匀地分散到其他节点上去
缓存服务器的优雅扩容
1、高频Key预热:
实现:
负载均衡器作为路由层,是可以收集并统计每个缓存Key的访问频率的,如果能够维护一份高频访问Key的列表,
新的集群在启动时根据这个列表提前拉取对应Key的缓存值进行预热,便可以大大减少因为新增集群而导致的Key失效。
问题:
高频Key本身的缓存失效时间可能很短,预热时储存的Value在实际被访问到时可能已经被更新或者失效,处理不当会导致出现脏数据。
2.历史Hash环保留:
新增节点后,它所对应的Key在原来的节点还会保留一段时间。因此在扩容的延迟时间段,如果对应的Key缓存在新节点上还没有被加载,可以去原有的节点上尝试读取。
eg:
假设我们原有3个集群,现在要扩展到6个集群,这就意味着原有50%的Key都会失效(被转移到新节点上),如果我们维护扩容前和扩容后的两个Hash环,
在扩容后的Hash环上找不到Key的储存时,先转向扩容前的Hash环寻找一波,如果能够找到就返回对应的值并将该缓存写入新的节点上,找不到时再透过缓存
缺点:
增加了缓存读取的时间
好处:
相比于直接击穿缓存而言还是要好很多的,可以随意扩容多台机器,而不会产生大面积的缓存失效。
Redis分布式集群方案-HashSlot算法
HashSlot算法
1、默认分配了16384个Slot(这个大小正好可以使用2kb的空间保存),每个Slot相当于一致性Hash环上的一个节点。
2、集群的所有实例将均匀地占有这些Slot
3、当我们Set一个Key时,使用CRC16(Key) % 16384来计算出这个Key属于哪个Slot,并最终映射到对应的实例上去。
问题一 当增删实例时,Slot和实例间的对应要如何进行对应的改动呢?
1、原本有3个节点A,B,C,那么一开始创建集群时Slot的覆盖情况是: 节点A 0-5460 节点B 5461-10922 节点C 10923-16383
2、增加一个节点D RedisCluster的做法是将之前每台机器上的一部分Slot移动到D上(注意这个过程也意味着要对节点D写入的KV储存),成功接入后Slot的覆盖情况将变为如下情况: 节点A 1365-5460 节点B 6827-10922 节点C 12288-16383 节点D 0-1364,5461-6826,10923-1228
3、删除一个节点 将其原来占有的Slot以及对应的KV储存均匀地归还给其他节点。
问题二 节点寻找?
1、每个节点都保存有完整的HashSlot - 节点映射表 每个节点都知道自己拥有哪些Slot,以及某个确定的Slot究竟对应着哪个节点。
2、无论向哪个节点发出寻找Key的请求,该节点都会通过CRC(Key) % 16384计算该Key究竟存在于哪个Slot,并将请求转发至该Slot所在的节点。
精髓 映射表和内部转发,通过著名的**Gossip协议**来实现的。
Redis持久化方式?
1、RDB (快照存储持久化方式):
将Redis某一时刻的内存数据保存到硬盘的文件当中,默认保存的文件名为dump.rdb,而在Redis服务器启动时,会重新加载dump.rdb文件的数据到内存当中恢复数据。
开启方式:save 和 bgsave命令
save:
当客户端向服务器发送save命令请求进行持久化时,服务器会阻塞save命令之后的其他客户端的请求,直到数据同步完成。
如果数据量太大,同步数据会执行很久,而这期间Redis服务器也无法接收其他请求,所以,最好不要在生产环境使用save命令。
bgsave:
一个异步操作
1 客户端向服务器发送bgsave命令
2 Redis服务器主进程会fork一个子进程来数据同步
3 数据保存到rdb文件之后,子进程退出
优点:
采用fork子进程,主进程仍然可以接收请求,但是fork子进程是一个同步操作,仍然在fork时无法接收其他请求,所以如果fork子进程耗时大的情况下,仍然无法接收其他客户端请求。
flushall:
生成一个空的dump.rdb文件
服务器配置自动触发:与bgsave一样
1、redis.conf
# 900s内至少达到一条写命令 save 900 1 # 300s内至少达至10条写命令 save 300 10 # 60s内至少达到10000条写命令 save 60 10000 # 启动服务器加载配置文件 redis-server redis.conf
rdb文件生成过程:
1、生成临时的rdb文件,并写入数据
2、完成写入,用临时rdb文件替换正式rdb文件
3、删除原来的rdb文件
4、默认文件名为dump.rdb(可以在配置文件中重新命名)
2、AOF
AOF持久化方式会记录客户端对服务器的每一次写操作命令,并将这些写操作以Redis协议追加保存到以后缀为aof文件末尾,在Redis服务器重启时,会加载并运行aof文件的命令,以达到恢复数据的目的。
开启:
# 开启aof机制 appendonly yes # aof文件名 appendfilename "appendonly.aof" # 写入策略,always表示每个写操作都保存到aof文件中,也可以是everysec或no appendfsync always //每个写命令都保存到aof文件,频繁,安全,io高 # appendfsync everysec //每秒写一次aof文件,最多有1s的丢失 # appendfsync no //Redis不负责写入aof,而是交由操作系统来处理什么时候写入aof文件。更快,但也是最不安全的选择,不推荐使用。 # 默认不重写aof文件 no-appendfsync-on-rewrite no # 保存目录 dir ~/redis/
缓存淘汰算法
1、FIFO 先进先出
2、LRU 最近最久未使用
3、LFU 最不经常使用,一段时间内使用最少,优先淘汰
缓存淘汰策略
1、noeviction 不删除,如达到最大内存,直接抛出异常
2、allkeys-lru 所有key优先删除最近最少使用
3、volatile-lru 优先删除最近最少使用
4、allkeys-random 所有key随机删除一部分key
5、volidate-random 只限制设置expire随机一部分key
6、volidate-all 只限制设置了expire优先删除剩余时间最短的key
Redis过期键删除策略
1,定时删除:实现方式,创建定时器
2,惰性删除:每次获取键时,检查是否过期
3,定期删除:每隔一段时间,对数据库进行一次检查,删除过期键,由算法决定删除多少过期键和检查多少数据库
为什么不适用自动删除策略
1、消耗cpu
2、并发的时候,cpu的时间应用于处理请求,而非在某一时刻删除redis中的key
定期删除
Redis默认100ms检查,判断时候有过期key,就删除。在检查时会随机抽取。
因为是随机抽取,会导致数据到时间没有删除。