1.基本数据类型和使用场景
1.1 string
作为常规的key-value缓存应用。
例如微博数、粉丝数等
bitmap:用一个数组表示一个范围,每个范围值用0和1进行标识
获取和设置的时间复杂度都是o(1)
设置时bitmap数组可能会发生长度的变化,当偏移量最大值不满足条件时
存储两种状态:登录/未登录 签到/未签到 打卡/未打卡
setbit tabName user_id 0/1 设置登录 //将user_id记为偏移量offset (offset从0开始)
getbit tabName user_id // 获取某个用户是否登录
bitcount tabName //获取某天总共有多少登录的人
list和bitmap占用空间,list会按照实际登录用户数存储,
而bitmap会根据最大的用户ID去分配空间
假设用户id是自增的,有8亿用户,活跃有一亿
list存储:每个用户id假设是int类型,32位 321亿=400MB
bitmap存储:8亿用户,就会有8亿bit 1bit8亿=100MB
假设用户id是自增的,有8亿用户,活跃只有1百万
list存储:每个用户id假设是int类型,32位 32100万=4MB
bitmap存储:8亿用户,就会有8亿bit 1bit8亿=100MB
由此可见,并不是使用bitmap都是最好的选择,只有当活跃用户较多时使用bitmap才是最好的选择。
记录用户每月签到的天数,使用32bit即可,每个bit位是0或者1
用户2月17号签到
SETBIT u:sign:1000:201902 16 1 # 偏移量是从0开始,所以要把17减1
检查2月17号是否签到
GETBIT u:sign:1000:201902 16 # 偏移量是从0开始,所以要把17减1
统计2月份的签到次数
BITCOUNT u:sign:1000:201902
获取2月份前28天的签到数据
BITFIELD u:sign:1000:201902 get u28 0
获取2月份首次签到的日期
BITPOS u:sign:1000:201902 1 # 返回的首次签到的偏移量,加上1即为当月的某一天
bitmap.png1.2 hash
hash是一个string类型的field和value的映射表,hash特别适合用于存储对象(因为对象可能会包含很多属性)
1.3 set
在微博中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合,提供了交集、并集、差集的操作。可以非常方便的实现如共同关注、共同喜好、二度好友
1.4 zset
zset的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,跳跃表按score从小到大保存所有集合元素。使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。时间复杂度与红黑树相同,增加、删除的操作较为简单,跳表空间换时间.
应用:排行榜,延时队列。
1.5 list
list列表是简单的字符串列表,按照插入顺序排序(内部实现为LinkedList),可以选择将一个链表插入到头部或尾部。
常用命令 :lpush(添加左边元素),rpush,lpop(移除左边第一个元素),rpop,lrange(获取列表片段,LRANGE key start stop)
应用:做简单的消息队列
2.持久化方式
2.1.RDB
对redis中的数据执行周期性持久化,隔一段时间存储
当满足条件时,redis需要执行RDB的时候服务器会执行以下操作:
1.redis调用系统的fork()函数创建一个子进程
2.子进程将数据集写入一个临时的RDB文件
3.当子进程完成对临时的RDB文件的写入时,redis用新的RDB文件来替换原来旧的RDB文件,并将旧的RDB文件删除
优点:生成多个文件,每个文件都代表了某一时刻中redis的数据;RDB对redis对外提供读写服务的时候,影响非常小,因为redis 主进程只需要fork一个子进程出来让子进程对磁盘io来进行rdb持久化;RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
(适合大规模的数据恢复;对数据完整性和一致性要求不高;恢复速度比AOF快)
缺点:在时间间隔内服务挂了会丢失部分数据;RDB每次fork出子进程来执行RDB快照生成文件时,如果文件特别大,可能会导致客户端提供服务暂停数毫秒或者几秒(fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑)
2.2 AOF
以日志的形式来记录每个写操作,所以会不断的增大,当大到一定程度时,AOF会做rewrite操作,rewrite操作就是基于当时redis的数据重新构造一个小的AOF文件,然后将大的AOF文件删除.
优点:(1)更好的保护数据不丢失,通过后台的一个线程去执行一次fsync操作,如果redis挂掉,最多丢失1秒的数据,数据 的完整性和一致性更高
(2)AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。
(3)AOF日志文件的命令通过非常可读的方式进行记录,这个非常适合做灾难性的误删除紧急恢复,如果某人不小心用 flushall命令清空了所有数据,只要这个时候还没有执行rewrite,那么就可以将日志文件中的flushall删除,进行恢复。
rewrite:将过程化数据去掉,比如相同的key,设置多次值,只需要最后的设置命令
缺点:对于同一份文件AOF文件比RDB数据快照要大;AOF开启后支持写的QPS会比RDB支持的写的QPS低,因为AOF一般会配置成每秒fsync操作,每秒的fsync操作还是很高的;数据恢复比较慢,不适合做冷备.
Always:同步写回 每次操作命令执行完后,同步将 AOF 日志数据写回硬盘;
Everysec:每秒写回 每次操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
No:操作系统控制的写回 Redis 不在控制命令的写会时机,交由系统控制。每次操作命令执行完成之后,命令会被放入到 AOF 文件的内核缓冲区,之后什么时候写入到磁盘,交由系统控制。
如果同时使用RDB和AOF两种持久化机制,那么redis重启的时候,会使用AOF来构建数据,因为AOF的数据更加完整
两种方式都要使用:RDB快速恢复(性能),AOF恢复完整数据
redis4.0支持混合持久化方式
3.redis为什么这么快
3.1完全基于内存,绝大部分请求都是纯粹的内存操作,非常快速,查找和操作的时间复杂度都是
3.2.数据结构简单,对数据操作简单(string hash list set zset)
3.3采用单线程,避免了不必要的上下文切换和资源竞争,也不存在多进程或者多线程导致的切换而消耗cpu,不用考虑锁以及锁带来的性能消耗。不过6.0高版本的好像支持了多线程,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行redis命令仍然是单线程顺序执行。
3.4.使用多路i/o复用模型,非阻塞i/o
什么是多路复用?
redis的性能瓶颈在内存和网络I/O,并不是CPU
网路I/O
五种网络I/O模型:
五种网络I/O模型.png 多路复用.png
select :
一个线程收集文件描述符,存在一个数组里面,传入内核态,由内核遍历哪些文件描述符有读写事件,有的话就修改相应文件描述符的标识,然后返回,哪些有数据返回还是需要用户态select自己遍历找出,然后读取相应的数据到缓冲区。
缺点:数组大小有限制(32位1024,64位2048);传入数据有用户态到内核态的拷贝开销;文件描述符结果需要遍历,有o(n)的时间复杂度。
bitmap数组存储的是1024个文件描述的状态,数组的下标表示文件描述符,1和0表示是否需要监听
reset是否有数据
poll
将数组改成了链表,去掉了监听文件描述符个数的限制
创建事件
不需要重置reset位
epoll
epoll_create 创建文件句柄对象
epoll_ctl 添加要管理的文件描述符
epoll_wait 阻塞等待文件描述符I/O事件
传入的是一个链表,去除了文件描述符的数量限制
内核保存一份文件描述符集合,每次只需告诉内核修改部分(减少拷贝)
内核只返回准备就绪的文件描述符(回把就绪的文件描述符放到最前面,并告诉有几个就绪的文件描述符,减少遍历)
内核不再采用轮询遍历的方式找到就绪的文件描述符,而是通过异步I/O的方式
3.5二进制安全
3.6内存预分配机制
成倍分配,避免频繁分配内存;超过1M就不成倍频繁分配,每次增加1M
一致性hash算法:一个hash环,计算数据的hash值,顺时针碰到的第一台服务器就是应该访问的服务器,当增加或者减少服务器的时候只会影响hash环中少部分数据,减少服务器往前一台服务器移动,增加服务器只影响逆时针部分的hash数据。
数据倾斜问题:设置虚拟节点。对一台服务器设置几个虚拟节点,防止因为服务器数较少造成数据倾斜。
image.png
一致性hash算法的粗略实现
4.雪崩、击穿、穿透
4.1缓存雪崩:
缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。
解决方案:过期时间加一个随机值的短时间
4.2缓存穿透:
查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机
解决方案:1.设置空值到缓存,短时间过期
2.运维在nginx配置中限制单个ip每秒访问次数超出拉黑ip
3.拦截校验非法参数(比如用户id等)
4.布隆过滤器:位数组存储,多个hash函数计算不同hash值,汇总多个值的判断结果,下标作为hash值,存在设置相应位为1,不存在就是默认0,如果相应位置为0,则肯定不存在,所有位置都为1,则可能存在,存在误杀的可能。
5.利用互斥锁
4.3缓存击穿:
某一个key非常热点,过期时大并发查询穿破缓存,直接查询数据库
解决方案:
1.设置key永不过期
2.加互斥锁
5.数据倾斜如何解决、大key的发现和删除
数据倾斜原因:
5.1 存在bigkey
业务层避免创建bigkey,把集合类型的bigkey拆分成多个小集合,分散保存
bigkey 保存了大量集合元素(集合类型),会导致这个实例的数据量增加,内存资源消耗也相应增加。bigkey 的操作一般都会造成实例 IO 线程阻塞,如果 bigkey 的访问量比较大,就会影响到这个实例上的其它请求被处理的速度。
5.2 slot手工分配不均匀
避免把较多的slot分配到一个实例上,进行槽的迁移
如何删除大key:直接删除很耗时,造成主线程阻塞,崩溃,应用程序异常
1.SCAN命令分钟多次删除 需要借助程序
2.创建一个子线程去删除
6.redis底层数据结构
Redis的字典底层使用哈希表实现,每个字典通常有两个哈希表,一个平时使用,另一个用于rehash时使用,使用链地址法解决哈希冲突。
跳跃表通常是有序集合的底层实现之一,表中的节点按照分值大小进行排序。
整数集合是集合键的底层实现之一,底层由数组构成,升级特性能尽可能的节省内存。
压缩列表是Redis为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实现之一
zset 可能使用压缩列表或者跳表o(n)
zset 跳表o(logN):在链表的基础上建立一些索引,可以快速的进行插入、删除、查找操作,加快查找速度
6.1 hash
哈希表(字典实现的哈希对象)、压缩列表o(N)
6.2 list
压缩表(ziplist)、双向链表(linkedlist) o(N)
6.3 集合set
哈希表、 整数集合实现的集合对象(INTSET) o(N)
6.4 zset
压缩列表、跳跃表和字典实现(空间换时间)分数组成跳跃表
6.5 string
SDS字符串,有长度,空闲空间,以\0结尾
字符串对象的编码可以是int、raw或者embstr
字符串对象为整数值,且可用long类型来表示,则ptr将转换成long,编码为int
字符串对象为字符串值,且长度>32字节,则字符串对象将使用一个SDS来保存字符串值,编码为raw
字符串对象为字符串值,且长度<=32字节,则字符串对象将使用embstr编码来保存字符串值
压缩列表:查找比较块,插入较慢,空间比较少,将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
跳表:时间性能快,空间耗费更多,空间换时间
哈希函数特点:
1.相同的输入一定对应着一个相同的输出
2.不同的输入可能会产生相同的输出,哈希冲突
redis哈希冲突:采用链地址法解决hash冲突,就是一个链表,hashTable
redis默认支持16个数据库
redis中key都是string类型
搬移数据:定时任务和访问的时候进行数据搬移
先访问老的数据,插入直接插入新的存储空间
rehash 渐进式扩容
type key查看key的存储类型
object encoding key 查看key的编码
redis编码.png
7.redis实现简单的分布式锁
1.setnx 设置成功就是不存在 多机器部署的情况下一个进程挂了,会有死锁的可能
2.set key value nx ex expireTime 过期时间,防止死锁
3.客户端1操作太久,业务没有执行完,导致锁自动释放,客户端2这时拿到了锁,
开始操作共享资源,这时客户端1操作完成,释放了客户端2的锁
4.设置一个锁的标识,是否是自己的锁,释放的时候先做一个判断,比如uuid
执行get del命令会有原子性问题
5.但是还是会有原子性问题,写一个lua脚本
因为 Redis 处理每一个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成
6.过期时间问题,尽量冗余过期时间,但是还是解决不了问题
7.加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间,Redisson
8.主从集群,主库突然挂了,切换了主节点,set设置锁的命令还没有同步到从库,新的从库变成了主库,未同步到这个锁,其他客户端还是会拿到这个锁
9.redlock红锁: 解决redis单机故障问题
(客户端实现的一个redlock算法,部署多台redis,同时向多台机器请求拿锁,如果超过半数以上拿到锁,则拿锁成功)
大致过程:
依次对多个 Redis 实例进行加锁(一般是3个或5个),加锁命令使用单实例 Redis 的加锁命令;
为了避免在某个节点长时间获取不到锁而阻塞,每次获取锁操作也有一个超时时间,远小于 TTL,超过超时时间则认为失败,继续向下一个节点获取锁;
计算整个获取多把锁的总消耗时间,只有在超过半数节点都成功获取锁,并且总消耗时间小于 TTL,则认为成功持有锁;
成功获取锁后,要重新计算 TTL = TTL - 总消耗时间;
如果获取锁失败,要向所有 redis 实例发送释放锁的命令。
释放锁操作就是向所有实例都发送删除 key 命令。
Redlock 容错性依赖于一个时间戳的计算,这在分布式系统中并不受待见,于是有了一场著名的论战。
ZooKeeper:分布式锁为什么要用到zk?
8.如何保证redis缓存和mysql数据库数据一致
在高并发应用场景下,如果是对数据一致性要求高的情况下,要定位好导致数据和缓存不一致的原因
解决高并发场景下数据一致性的方案有两种,分别是延时双删策略和异步更新缓存两种方案
延时双删:先删除redis缓存,再执行数据库操作,执行完数据库操作后再删除一次redis缓存
异步更新缓存:订阅binlog日志,通过消息队列将数据同步到redis缓存
另外,设置缓存的过期时间是保证数据保持一致性的关键操作,需要结合业务进行合理的设置
9.redis集群
9.1 主从模式
1.避免Redis单点故障,解决高可用问题
2.构建读写分离架构,满足读多写少的应用场景
9.2 哨兵模式:sentinel
为啥要用哨兵模式?
主从结构中,如果主Redis宕机,如果主库挂了,需要运维人工接入,重新指定主节点
自动完成新的主库的切换
至少需要三个哨兵,进行投票选举主节点
什么是哨兵?
1.监控(Monitoring):Sentinel会不断地检查你的主服务器和从服务器是否运作正常
2.提醒(Notification):当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知
3.自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主
服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器
哨兵的作用:
集群监控:对 Redis 集群的主从进程进行监控,判断是否正常工作。
消息通知:如果存在 Redis 实例有故障,那么哨兵可以发送报警消息通知管理员。
故障转移:如果主机(master)节点挂了,那么可以自动转移到从(slave)节点上。
配置中心:当存在故障时,对故障进行转移后,配置中心会通知客户端新的主机(master)地址。
9.3高可用集群
Redis3.0版本之后支持Cluster模式
高可用架构.png
(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的master节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->key
Redis 的集群的功能是为了解决单机 Redis 容量有限的问题,将数据按一定的规则分配到多台机器,对内存的每秒访问不受限于单台服务器,可受益于分布式集群高扩展性。
redis集群是一个无中心的分布式Redis存储架构,可以在多个节点之间进行数据共享,解决了Redis高可用、可扩展
1、将数据自动切分(split)到多个节点
2、当集群中的某一个节点故障时,redis还可以继续处理客户端的请求。
一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个数据都属于这16384个哈希槽中的一个。集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽。集群中的每一个节点负责处理一部分哈希槽。
考虑故障迁移数据问题和数据倾斜问题?
采用一致性hash算法和设置虚拟节点的方式解决问题。
为什么是 16384 呢?
主要考虑集群内的网络带宽,而 16384 刚好是 2K 字节大小。
10.redis缓存策略
10.1 redis 提供了 3 种删除策略:
被动删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key。
主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期主动淘汰一批已过期的 key。
主动删除:当前已用内存超过 maxmemory 限定时,触发主动清理策略。
10.2 Redis 有哪几种数据“淘汰”策略
Redis一共有几种数据淘汰策略:(redis4.0之前是6种,4.0之后又增加了两种淘汰策略,LFU 使用频率最少)
(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
(4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
(5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
(6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
(7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
(8) no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
这八种大体上可以分为4中,lru、lfu、random、ttl。
LRU算法:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的key。
LFU算法,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的key。
网友评论