Redis
1.Redis的基本数据类型:
- 五种基本数据类型:String(字符串)、Hash(哈希)、List(集合)、Set(集合)、Zset(有序集合)
- 三种特殊的数据结构类型:Geospatial、Hyperloglog、Bitmap。
Geospatial:Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作;
HyperLogLog:用来做基数统计算法的数据结构,如统计网站的UV;
Bitmaps :用一个比特位来映射某个元素的状态,在Redis中,它的底层是基于字符串类型实现的,可以把bitmaps成作一个以比特位为单位的数组;
2.Redis为什么快:
- 基于内存实现,省去磁盘I/O的消耗。
- 高效的数据结构:SDS简单动态字符串、双端链表、压缩链表、字典、跳跃表:
动态字符串:
1)字符串长度处理:Redis获取字符串长度,时间复杂度为O(1),而C语言中,需要从头开始遍历,复杂度为O(n);
2)空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗;
3)惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配;
4)二进制安全:Redis可以存储一些二进制数据,在C语言中字符串遇到’\0’会结束,而 SDS中标志字符串结束的是len属性。
双端链表:
双端链表与单链表的区别在于它不只第一个链结点有引用,还对最后一个链结点有引用。
压缩链表:
为了解决经典双端链表在保存小数据而导致内存效率过低的问题,Redis设了一套压缩链表的数据数据结构ziplist来对这种场景下的链表应用进行优化,压缩链表与经典双端链表最大的区别在于,双端链表的节点是分散在内存中并不是连续的,压缩链表中所有的数据都是存储在一段连续的内存之中的。
字典:
字典就是哈希表,比如HashMap,通过key就可以直接获取到对应的value。而哈希表的特性,在O(1)时间复杂度就可以获得对应的值。
跳跃表:
1)跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率;
2)跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。 - 合理的数据编码:Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。
String: 如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码;
List: 如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码;
Hash: 哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码;
Set: 如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码;
Zset: 当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码。 - 合理的线程模型:
I/O 多路复用:多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
什么是I/O多路复用?
I/O :网络 I/O
多路 :多个网络连接
复用:复用同一个线程
IO多路复用其实就是一种同步IO模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出cpu。 - 单线程模型:
1)Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。Redis是面向快速执行场景的数据库。,所以要慎用如smembers和lrange、hgetall等命令。
2)Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程。 - 虚拟内存机制:
Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
Redis的虚拟内存机制是啥呢?
虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
缓存击穿、缓存穿透、缓存雪崩
3.缓存击穿、缓存穿透、缓存雪崩:
- 缓存穿透问题
读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。
如何避免缓存穿透呢?
1.如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。
3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
布隆过滤器原理: 它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
- 缓存雪崩问题
指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。
如何避免缓存雪崩呢?
1.缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒。
2.Redis 故障宕机也可能引起缓存雪崩。这就需要构造Redis高可用集群啦。
- 缓存击穿问题
指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求达到db。
如何避免缓存击穿呢?
1.使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
- “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
缓存雪崩和缓存击穿有什么区别?
区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪崩的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点key缓存,雪奔则是很多key
4.Redis的常用应用场景:
缓存、排行榜、共享Session、分布式锁、社交网络、位操作
缓存: 合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以降低数据库DB的压力;
排行榜: Redis提供的zset数据类型能够实现这些复杂的排行榜;
共享Session: 如果一个分布式Web服务将用户的Session信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用Redis将用户的Session进行集中管理,每次用户更新或者查询登录信息都直接从Redis中集中获取;
分布式锁: 可以用Redis的setnx来实现分布式的锁;
位操作: 用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多。这里要用到位操作——使用setbit、getbit、bitcount命令。原理是:redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统。
5.Redis 的持久化机制:
Redis提供了RDB和AOF两种持久化机制。
持久化文件加载流程如下:
- RDB
就是把内存数据以快照的形式保存到磁盘上。RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。RDB触发机制主要有:手动触发、自动触发。
手动触发:save — 同步,会阻塞当前redis服务器;bgsave — 异步,redis进程执行fork操作创建子进程。
自动触发:save m n — m 秒内数据集存在 n 次修改自动执行 bgsave 操作。
优点: 适合大规模的数据恢复场景,如备份,全量复制等。
缺点: 没办法做到实时持久化/秒级持久化;新老版本存在RDB格式兼容问题。 - AOF
采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。
优点: 数据的一致性和完整性更高;
缺点: AOF记录的内容越多,文件越大,数据恢复变慢。
6.MySQL与Redis 如何保证双写一致性
- 实时一致性方案:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
- 最终一致性方案:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。
– 根据是否接收写请求,可以把缓存分成读写缓存和只读缓存。
只读缓存:只在缓存进行数据查找,即使用“更新数据库+删除缓存”策略。
读写缓存:需要在缓存中对数据进行增删改查,即使用“更新数据库+更新缓存”策略。
- 只读缓存策略: 先删除缓存,后更新数据库/先更新数据库,后删除缓存
(无并发:
a.消息队列+异步重试: 无论使用哪一种执行时序,可以在执行步骤1时,将步骤2的请求写入消息队列,当步骤2失败时,就可以使用重试策略,对失败操作进行“补偿”。
具体步骤如下:
1)把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka消息队列)
2)当删除缓存值或者是更新数据库值成功时,把这些值从消息队列中去除,以免重复操作。
3)当删除缓存值或者是更新数据库值失败时,执行失败策略,重试服务从消息队列中重新读取这些值,然后再次进行删除或更新。
4)删除或者更新失败时,需要再次进行重试,重试超过的一定次数。向业务层发送报错信息。
b.订阅Binlog变更日志: 不管用MQ/Canal或者MQ+Canal的策略来异步更新缓存,对整个更新服务的数据可靠性和实时性要求都比较高,如果产生数据丢失或者更新延时情况,会造成MySQL和Redis中的数据不一致。因此,使用这种策略时,需要考虑出现不同步问题时的降级或补偿方案。
具体操作步骤如下:
1)创建更新缓存服务,接收数据变更的MQ消息,然后消费消息,更新/删除Redis中的缓存数据。
2)使用Binlog实时更新/删除Redis缓存。利用Canal,即将负责更新缓存的服务伪装成一个MySQL的从节点,从MySQL接收Binlog,解析Binlog之后,得到实时的数据变更信息,然后根据变更信息去更新/删除Redis缓存。
3)MQ+Canal策略,将Canal Server接收到的Binlog数据直接投递到MQ进行解耦,使用MQ异步消费Binlog日志,以此进行数据同步。
(高并发:
使用以上策略后,可以保证在单线程/无并发场景下的数据一致性。但是,在高并发场景下,由于数据库层面的读写并发,会引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了)
先删除缓存,再更新数据库:假设线程A删除缓存值后,由于网络延迟等原因导致未及更新数据库,而此时,线程B开始读取数据时会发现缓存缺失,进而去查询数据库。而当线程B从数据库读取完数据、更新了缓存后,线程A才开始更新数据库,此时,会导致缓存中的数据是旧值,而数据库中的是最新值,产生“数据不一致”。其本质就是,本应后发生的“B线程-读请求”先于“A线程-写请求”执行并返回了。
设置缓存过期时间+延时双删: 通过设置缓存过期时间,若发生上述淘汰缓存失败的情况,则在缓存过期后,读请求仍然可以从DB中读取最新数据并更新缓存,可减小数据不一致的影响范围。虽然在一定时间范围内数据有差异,但可以保证数据的最终一致性。
此外,还可以通过延时双删进行保障:在线程A更新完数据库值以后,让它先sleep一小段时间,确保线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。后续其它线程读取数据时,发现缓存缺失,会从数据库中读取最新值。
sleep时间:在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。
如果难以接受sleep这种写法,可以使用延时队列进行替代。
先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。
先更新数据库,再删除缓存: 如果线程A更新了数据库中的值,但还没来得及删除缓存值,线程B就开始读取数据了,那么此时,线程B查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。其本质也是,本应后发生的“B线程-读请求”先于“A线程-删除缓存”执行并返回了。
a.延迟消息: 凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率。
b.订阅binlog,异步删除: 通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。
c.删除消息写入数据库: 通过比对数据库中的数据,进行删除确认 先更新数据库再删除缓存,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。
d.加锁: 更新数据时,加写锁;查询数据时,加读锁。
建议:
优先使用“先更新数据库再删除缓存”的执行时序,原因主要有两个:
1)先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力。
2)业务应用中读取数据库和写缓存的时间有时不好估算,进而导致延迟双删中的sleep时间不好设置。 - 针对读写缓存: 更新数据库+更新缓存
– 读写缓存:增删改在缓存中进行,并采取相应的回写策略,同步数据到数据库中。
– 同步直写:使用事务,保证缓存和数据更新的原子性,并进行失败重试(如果Redis本身出现故障,会降低服务的性能和可用性)
– 异步回写:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库(没写回数据库前,缓存发生故障,会造成数据丢失)
该策略在秒杀场中有见到过,业务层直接对缓存中的秒杀商品库存信息进行操作,一段时间后再回写数据库。
– 一致性:同步直写>异步回写,因此,对于读写缓存,要保持数据强一致性的主要思路是:利用同步直写,同步直写也存在两个操作的时序问题:更新数据库和更新缓存。
(无并发: 先更新缓存,后更新数据库/先更新数据库,后更新缓存,某一步骤操作失败,造成缓存和数据库数据不一致问题。
解决策略:
a.消息队列+重试机制
b.消息队列+重试机制(订阅Binlog日志)
(高并发:
针对场景1和2的解决方案是:保存请求对缓存的读取记录,延时消息比较,发现不一致后,做业务补偿
针对场景3和4的解决方案是:对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,保证同一时间只有一个线程去更新数据库和缓存;没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。
其中,分布式锁的实现可以使用以下策略:
强一致性策略
上述策略只能保证数据的最终一致性。要想做到强一致,最常见的方案是2PC、3PC、Paxos、Raft这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。如果业务层要求必须读取数据的强一致性,可以采取以下策略:
1)暂存并发读请求
在更新数据库时,先在Redis缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。
2)串行化
读写请求入队列,工作线程从队列中取任务来依次执行
(1)修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上。
(2)修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的。
(3)使用Redis分布式读写锁
将淘汰缓存与更新库表放入同一把写锁中,与其它读请求互斥,防止其间产生旧数据。读写互斥、写写互斥、读读共享,可满足读多写少的场景数据一致,也保证了并发性。并根据逻辑平均运行时间、响应超时时间来确定过期时间。
较为通用的一致性策略拟定:
在并发场景下,使用“更新数据库+更新缓存”需要用分布式锁保证缓存和数据一致性,且可能存在“缓存资源浪费”和“机器性能浪费”的情况;一般推荐使用“更新数据库+删除缓存”的方案。如果根据需要,热点数据较多,可以使用“更新数据库+更新缓存”策略。
在“更新数据库+删除缓存”的方案中,推荐使用推荐用“先更新数据库,再删除缓存”策略,因为先删除缓存可能会导致大量请求落到数据库,而且延迟双删的时间很难评估。
在“先更新数据库,再删除缓存”策略中,可以使用“消息队列+重试机制”的方案保证缓存的删除。并通过“订阅binlog”进行缓存比对,加上一层保障。
此外,需要通过初始化缓存预热、多数据源触发、延迟消息比对等策略进行辅助和补偿。【多种数据更新触发源:定时任务扫描,业务系统MQ、binlog变更MQ,相互之间作为互补来保证数据不会漏更新】。
7.Redi事务
1、Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
2、一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
3、单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
8.Redis分布式锁
redis是一个单独的非业务服务,不会受到其他业务服务的限制,所有的业务服务都可以向redis发送写入命令,且只有一个业务服务可以写入命令成功,那么这个写入命令成功的服务即获得了锁,可以进行后续对资源的操作,其他未写入成功的服务,则进行其他处理。
实现:
获取锁: setnx命令:表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做;
客户端1和2 同时向redis写入try_lock,客户端1写入成功,即获取分布式锁成功。客户端2写入失败,则获取分布式锁失败。
释放锁: 当客户端1操作完后,释放锁资源,即删除try_lock。那么此时客户端2再次尝试获取锁时,则会获取锁成功。
避免死锁: 假如客户端1在获取到锁资源后,服务宕机了,那么这个try_lock会一直存在redis中,那么其他服务就永远无法获取到锁了,既造成了死锁。
解决: 设置键过期时间,超过这个时间即给key删除掉。
锁的过期时间设置: 假如服务A加锁成功,锁会在10s后自动释放,但由于业务复杂,执行时间过长,10s内还没执行完,此时锁已经被redis自动释放掉了。此时服务B就重新获取到了该锁,服务B开始执行他的业务,服务A在执行到第12s的时候执行完了,那么服务A会去释放锁,则此时释放的却是服务B刚获取到的锁。
解决: 在锁将要过期的时候,如果服务还没有处理完业务,那么将这个锁再续一段时间。比如设置key在10s后过期,那么再开启一个守护线程,在第8s的时候检测服务是否处理完,如果没有,则将这个key再续10s后过期。
在Redisson(Redis SDK客户端)中,就已经帮我们实现了这个功能,这个自动续时的我们称其为”看门狗’”。
redis宕机: 引入redis集群,但是涉及到redis集群,就会有新的问题出现,假设是主从集群,且主从数据并不是强一致性。当主节点宕机后,主节点的数据还未来得及同步到从节点,进行主从切换后,新的主节点并没有老的主节点的全部数据,这就会导致刚写入到老的主节点的锁在新的主节点并没有,其他服务来获取锁时还是会加锁成功。此时则会有2个服务都可以操作公共资源,此时的分布式锁则是不安全的。redis的作者也想到这个问题,于是他发明了RedLock。
实现RedLock: 要实现RedLock,需要至少5个实例(官方推荐),且每个实例都是master,不需要从库和哨兵。
流程:
1、客户端先获取当前时间戳T1;
2、客户端依次向5个master实例发起加锁命令,且每个请求都会设置超时时间(毫秒级,注意:不是锁的超时时间),如果某一个master实例由于网络等原因导致加锁失败,则立即想下一个master实例申请加锁;
3、当客户端加锁成功的请求大于等于3个时,且再次获取当前时间戳T2,当时间戳T2 - 时间戳T1 < 锁的过期时间。则客户端加锁成功,否则失败;
4、加锁成功,开始操作公共资源,进行后续业务操作;
5、加锁失败,向所有redis节点发送锁释放命令。
即当客户端在大多数redis实例上申请加锁成功后,且加锁总耗时小于锁过期时间,则认为加锁成功。 释放锁需要向全部节点发送锁释放命令。
注: 第3步为啥要计算申请锁前后的总耗时与锁释放时间进行对比呢?
因为如果申请锁的总耗时已经超过了锁释放时间,那么可能前面申请redis的锁已经被释放掉了,保证不了大于等于3个实例都有锁存在了,锁也就没有意义了。
RedLock的问题:
1、得5个redis实例,成本大大增加;
2、可以通过上面的流程感受到,这个RedLock锁太重了;
3、主从切换这种场景绝大多数的时候不会碰到,偶尔碰到的话,保证最终的兜底操作;
4、分布式系统中的NPC问题。
解释npc问题:
N:Network Delay,网络延迟
P:Process Pause,进程暂停(GC)
C:Clock Drift,时钟漂移
举例:
1、客户端 1 请求锁定节点 A、B、C、D、E;
2、客户端 1 的拿到锁后,进入 GC(时间比较久);
3、所有 Redis 节点上的锁都过期了;
4、客户端 2 获取到了 A、B、C、D、E 上的锁;
5、客户端 1 GC 结束,认为成功获取锁;
6、客户端 2 也认为获取到了锁,发生【冲突】。