一、使用缓存解决了什么问题
二、redis
redis的线程模型:
- 文件事件处理器
包括多个socket,IO多路复用程序,文件事件分配器,事件处理器(包括命令请求处理器,命令回复处理器,连接应答器等)。
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。
//文件事件
当socket变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对redis执行connect操作),socket就会产生一个AE_READABLE事件。
当socket变得可写的时候(客户端对redis执行read操作),socket会产生一个AE_WRITABLE事件。
IO多路复用程序可以同时监听AE_REABLE和AE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLE和AE_WRITABLE两种事件,那么文件事件分派器优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。
//文件事件处理器
如果是客户端要连接redis,那么会为socket关联连接应答处理器
如果是客户端要写数据到redis,那么会为socket关联命令请求处理器
如果是客户端要从redis读数据,那么会为socket关联命令回复处理器
- 客户端和redis通信的流程
1.redis初始化的时候会将命令应答处理器和AE_READABLE事件相连接
2.客户端通过他的socket01向redis的serversocket发送请求,serversocket产生一个AE_READABLE事件,IO多路复用程序,监听到这个事件,把事件压到一个队列中,文件事件分派器从队列中拿到这个事件,分派给命令连接事件处理器,命令连接事件处理器产生和客户端相连接的socket01,然后把这个socket01的AE_REABLE事件与命令请求处理器连接。
3.这时客户端向redis发送请求(无论是读请求还是写请求),相对应的socket01产生AE_READABLE事件,通过对应的命令请求处理器连接,命令请求处理器执行相应的操作,然后把socket01的AE_READABLE事件与命令回复处理器关联。
4.redis准备好相应的数据后,会把socket的AE_WRITABLE与命令回复处理器相连,客户端准备好接收数据后,会和对应的socket01产生一个AE_WRITABLE,由对应的命令回复器处理,将准备好的数据写入socket,供客户端读取。命令回复处理器执行完之后,会删除这个socket的AE_WRITABLE的关联关系。
三、为什么redis是单线程效率还这么高
1.因为采用的是非阻塞的IO多路复用模型,只负责监听socket和压队列,不处理请求,可以监听很多个socket。真正的处理事件是事件处理器。
2.事件处理器是基于纯内存处理的,效率很高。
3.单线程反而避免了多线程频繁的上下文切换问题。
四、redis的数据类型及相应的应用场景
string:简单的kv缓存
hash:
类似于map的结构,一般存储对象,把一些简单的对象缓存起来,后续操作,可以直接仅仅修改这个字段中的某个值,其他不用变。
存到list里面,前端请求分页查询
常量 文章id
list:
1.有序列表,可以把list的格式放在redis里面;
2.可以基于list的lrange命令进行分页查询,可以从某个元素开始读多少个元素,可以用于类似微博那种不间断的分页查询;
3.可以用来搞一个简单的消息队列,从头进去从尾出来
set:
无序集合,自动去重
可以基于set将系统需要去重的数据扔进去,自动去重,虽然hashset也可以,但是系统是多个的,redis的set可以用于全局去重
比如查询两个微博大v粉丝的交际
zset:
可以排序的set
如果你想要根据时间对数据进行排序,只需要把时间作为分数写进去就可以了
当数量小于128长度小于64时采用的是ziplist,否则采用skiplist跳表
1.zset为什么选择跳表来实现有序集合?
二分法查找底层依赖的是数组随机访问的特性,只能依赖数组实现。链表要实现二分法依赖跳表,在链表每个节点上加上一个down属性,表明该节点在下一层级中的位置。跳表是多个层级的,假如每两个节点抽出一个来建立索引,第一层是n个节点,第二层是n/2个,最高一层是2个。高度计算2^h = n,h=logn;有了高度那么每一层要便利几个节点呢,因为是两个节点建立一个索引,所以每一层最多遍历3个节点,所以查找一个节点需要遍历的节点数为3*h,去掉常数时间复杂度即为o(logn)
2、zset底层数据结构?简单说说跳表底层的数据结构?
3、什么时候采用压缩列表、什么时候采用跳表呢?
4、跳表的时间复杂度
5、简单描述一下跳表如何查找某个元素呢?
6、zset为什么用跳表而不用二叉树或者红黑树呢?
跳表在区间查询的时候效率是高于红黑树的,它查找时,以O(logn)的时间复杂度定位到区间的起点,然后在原始链表往后遍历就可以了 ,其它插入和单个条件查询,更新两者的复杂度都是相同的O(logn)。
跳表的代码实现相对于红黑树而言更容易实现。
跳表更加灵活,它在并发环境下可以通过改变索引构建策略,有效平衡执行效率和内存消耗。红黑树的平衡通过左旋转和右旋转来实现平衡。
7、redis既然是通过内存实现的,那为什么还要使用跳表这么一个空间换时间的结果
在实际开发中,原始链表中存储的很可能是很大的对象,而索引结点只需要存储关键值(用来比较的值)和几个指针(找到下级索引的指针),并不需要存储原始链表中完整的对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。
当我们不停的往跳表中插入数据时,如果我们不更新索引,就可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表会退化成单链表。
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平滑,也就是说如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。
跳表是通过随机函数来维护前面提到的 `平衡性`。
我们往跳表中插入数据的时候,可以选择同时将这个数据插入到第几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
随机函数可以保证跳表的索引大小和数据大小的平衡性,不至于性能过度退化。
五、redis的过期策略
redis的内存是有限的,我们在set key时可以设置一个expire time
redis的定期删除:redis会默认每100ms定期选择一些数据,看他们是否过期,过期的删掉,但是仍然有很多过期的数据存在内存,这就需要使用redis的惰性删除。
redis的惰性删除:当你在查询这个数据的时候,redis会先判断这个数据是否过期,过期则删除,但是仍然会有占用内存较高的可能。
这时就要使用内存淘汰机制
1.noeviction:内存不足以容纳新写入数据时,报错(一般不用);
2.allkeys-lru:内存不足以容纳新写入数据时,删除使用次数最少的key;
3.allkeys-random:内存不足以容纳新写入数据时,随机移除某个key;
4.volatile-lru:内存不足以容纳新写入数据时,在设置了过期时间的键中,移除最少使用的key;
5.volatile-random:内存不足以容纳新写入数据时,在设置了过期时间的键中,随机移除一个key;
6.volatile-ttl:内存不足以容纳新写入数据时,在设置了过期时间的键中,有更早过期时间的key优先移除。
LFU策略的筛选规则:
当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。
如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
![淘汰策略](https://i-blog.csdnimg.cn/blog_migrate/f867b89aa89ba7e5bac9d2aaecc96b4c.png)
六、LRU
在一个链表中,如果查询哪个数据,就返回那个数据,并提到链表头,如果链表满了,再插入数据就删除链表尾部的数据,把新数据插入链表头;
七、怎么保证redis是高并发高可用的
redis不能支持高并发的瓶颈是:单击
,若是redis要支撑QPS达到10万+,就是需要读写分离
:架构做成主从架构,多个从节点(redis slave),让所有读走从节点,这样还有个好处就是可以水平扩容。
1.redis replication(主从复制)
(1)master->slave复制是异步的,slave node复制时不会停止master node工作的,slave node也不会停止,不过对外提供的是旧数据,但是复制完成时需要删除旧数据,这时会暂停对外服务;
(2)从redis2.8开始,slave node会周期性的确认自己每次复制的数据量
(3)/*redis做主从复制时必须要做master的持久化*/,因为若是master宕机重启,磁盘中没有数据,那么就会认为数据是空的,slave node从master同步的时候也会清空数据,造成100%数据丢失;
(4)master还要做备份方案,万一本地文件丢失,从备份中挑选一份rdb去恢复master
即使采用了后续讲解的高可用机制,slave node可以自动接管master node,但是也可能sentinal还没有检测到master failure,master node就自动重启了,还是可能导致上面的所有slave node数据清空故障
主从复制的过程:
//过程
启动一个slave node时会向master node发送一个psync命令
第一种情况就是slave node重新连接master node,那么master仅仅会复制给slave缺失的数据
第二种是这个slave node是新启动的,那么会触发一次full resynchronization,开始full resynchronization时,master会创建一个后台线程,开始生成一个RDB快照文件,同时还会从客户端收到的所有写命令缓存在内存中,RDB文件生成后,master会将这个RDB发送到slave node,先存到slave nade磁盘中,再读到内存中,而快照之后的写命令,会从master内存直接发送给slave,达到数据一致;
//断点续传
如果主从复制过程中,网络连接断掉了,那么可以接着上传复制的地方,继续复制,master node会在内存中创建一个backlog,master和slave都会保存一个replica offset还有一个masterid,offset就保存在backlog中,这样复制中断也会继续上次位置复制,如果找不到offset,就会执行一次resynchronization;
//无磁盘化复制
master直接在内存中创建rdb,然后发给slave,需要设置repl-diskless-sync;repl-diskless-sync-delay,等待一点时长再开始复制,因为要等更多的slave重新连接过来;
//过期key处理
slave不会过期key,只会等待master过期key,或者通过lru淘汰一个key,那么会模拟一条del命令发给slave。
2.sentinal node(哨兵)
redis高可用架构叫做故障转移failover,也可以叫做主备切换,把slave node切换为master node。
sentinal node(哨兵)就是用来监控master node健康情况和切换slave node到master node的还可以做消息通知和故障转移。
哨兵也是分布式的,作为一个集群去运行,互相协同工作
(1)哨兵至少需要三个实例,来保证自己的健壮性
(2)哨兵+redis主从部署架构是不保证数据0丢失的,只保证高可用性
若是两个哨兵的节点,设置quorm=1即只要有一个哨兵认为master挂掉了就会选举出一个哨兵进行故障转移,同时这个时候需要majority,2个哨兵的majority=2,(2,3,4的majority是2,5的majority是3)若是有master的哨兵的节点机器整体挂掉了,那么达不到majority,那么就不能完成故障转移。
经典的3个哨兵集群,设置quorm=2,majority=2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4USP6MMU-1689559482939)(img\redis主从复制和sentinal模式.png)]
3.在哨兵+主从复制情况下数据丢失情况
第一种:主从复制是异步的,在master node接收到消息后会向client返回一个成功信号,假如在master node向salve node复制之前master就挂掉了,那么哨兵会选举一个salve作为master,那么这个还没复制的数据就丢失了;
第二种:/*集群脑裂*/。出现了有相同性的相同工作的两个master节点,这种情况出现在,发生网络故障时候,导致client和master与所有的slave节点和sentinal cluster出现信号中断,这时哨兵就会认为master故障,在salve节点选举出一个新的master节点,就会出现两个master。
而这时client会继续向旧的master节点,这时会囤积一些数据,这时排除故障,把原来的master节点停掉或者变成slave节点,那么旧的master就会从新的master复制数据,导致一部分的数据丢失。
解决:
设置两个参数:
min-slaves-to-write 1
min-slaves-max-lag 10
1.减少异步复制的数据丢失
这是设置slave相对于master的延迟不能超过10s钟,一旦有一个slave延迟超过10s钟就,就认为可能master宕机后损失的数据太多,那么拒绝写请求,这样可以把master宕机时数据丢失控制在可控范围内;
2.减少脑裂造成的数据丢失
一旦slave延迟超过10s,client就不会往master,只会往master里面写10s数据,降低损失;
3.出现master拒绝写请求只会,client可以考虑做降级,写到本地磁盘,在client对外接收请求,再做降级,做限流,减慢请求涌入速度;或者client可以考虑把数据临时灌入一个kafka消息队列,每隔10分钟去队列里面取一次,尝试重新恢复master。
七、redis持久化机制
redis持久化的意义
如果不进行持久化,redis宕机了,重启之后,内存的数据就消息了,所以要在内存存储数据的时候,异步的把数据存储到磁盘中。redis持久化的意义主要在于故障恢复。同时持久化也是对高可用的支撑,避免redis没有数据,请求全部打到mysql。
RDB及数据丢失情况
周期性的对redis内存生成当前内存全量快照,把快照写到磁盘。
优点:
(1)RDB会生成多个代表某个时间快照的文件,可以做冷备,由redis固定时长,控制快照时间,可以写个脚本,直接把存放磁盘的数据上传到云服务中做备份。
(2)RDB直接存储数据,AOF恢复数据是要回放和执行所有指令的,RDB恢复数据更快;
(3)RDB每个一段时间才把数据写入内存,AOF虽然可以快速写入os cache,但是还是需要一定时间开销,相对于AOF,RDB对redis影响更小。
可以通过SAVE或者BGSAVE来生成RDB文件
SAVE会造成阻塞,在此期间redis不能处理任何请求
BGSAVE则是会fork一个子进程,来生成RDB快照
缺点:
(1)RDB丢失数据会更多,若是redis宕机可能会导致这次快照和上次快照之间数据丢失,不适合做第一优先恢复方案;
(2)RDB在每次开启子线程执行快照数据生成时,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,所以一般不能让RDB间隔时间过长。
AOF
AOF机制对每条写入命令做日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF中的写入指令来重新构造整个数据集;
内存到磁盘中间还有个os cache;
假如redis最大保存1G数据,数据达到很大的时候会自动LRU,删除一部分数据,当AOF膨胀到很大的时候,会产生一个新的AOF,这个AOF把库中的数据压缩成一个或者几个命令,替换掉原日志文件。
优点:
(1)可以更好的保存数据不丢失,每隔一秒钟AOF就会调用操作系统的fsync操作,强制将os cache中的数据刷入磁盘中。最多丢失1秒数据
(2)AOF以append-only的模式写入一个日志文件中,日志文件不容易破损,即使破损也很容易修复
(3)AOF即使文件太大,出现rewrite log操作,也不会对redis产生太大影响。
缺点:
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照更大;
(2)AOF开启后支持的写QPS会比RDB支持写的QPS低;因为AOF一般设置每秒一次fsync。
(3)做数据恢复较慢,做冷备不方便
综合使用
用AOF来保证数据不丢失,用RDB来做不同程度的冷备,做快速的数据恢复。
同时开启RDB和AOF,做数据恢复的时候,首先会选择AOF,因为AOF数据更完备。
八、redis cluster——redis集群架构
哨兵模式下redis的master node和slave node的数据是一样的,master能容纳多大内存,那么整个系统就有这么大的redis内存。redis cluster是横向扩容更多的master节点,每个redis实例上存储一部分数据,每个实例可以挂redis从实例,即使主实例挂了,那么会自动切换到从实例。支撑可以,支持读写分离。
8.1 分布式数据存储的核心算法
- hash算法
计算hash值,对节点数进行取模,选择节点。这种情况若是出现一个master宕机,那么其余的请求会对剩余的节点数取模,来获取缓存中的数据,但是对原来节点取模数和现在节点取模数是不一样的,会有很大一部分请求拿不到原来的数据,会去数据库获取数据,大量的请求打在数据区,是灾难性的。
- 一致性hash算法
所有的节点分布在一个圆上,假如3个节点,那么一个key过来以后,同样计算hash值,对应在圆上的点上,这些点会顺时针寻找距离自己最近的节点,这样如果一个节点宕机,那么只会有1/3的key取不到数据,比hash算法要好一些。
/*一致性hash算法,可能会有缓存热点的问题*/一致性hash算法,可能会有缓存热点的问题,可能集中在某个hash区间内的值特别多,那么会导致大量的数据都涌入同一个master内,造成master的热点问题,性能出现瓶颈。
解决:
给每个master做了均匀分布的虚拟节点,这样在每个区间内,大量的数据都会均匀分布到不同的节点内,而不是顺时针涌入同一个节点内,做到负载均衡。
- hash slot(hash槽)
redis用的算法,对每个key计算CRC16然后每次都对16384(2^14)取模,每个节点都有部分slot
任何一台机器宕机,也是对16384取模,和一致性hash算法差不多,宕机后,会快速的把原节点上的slot分配的另外两个节点,这样避免某台机器宕机,所有缓存都失效的问题。
gossip协议——redis采用的协议
所有节点都维护一份元数据,不同的节点如果出现了元数据的变更之后,就不断将元数据发送给其他节点,让其他节点也进行变更。
优点:元数据更新比较分散,不是集中在一个地方,更新请求断断续续,有一点延时,降低压力
缺点:更新有延时,可能导致集群中的一些操作会有些滞后
集中式元数据维护
所有的元数据都存储在zookeeper中,
优点在于,时效性好,通信及时
缺点在于,所有元数据存储在一个地方,可能导致元数据存储有压力
创建集群常见命令
1.$ redis-cli -c -p 7001:连接其中一个节点
2.CLUSTER MEET <ip> <port>:通过向一个节点 A 发送 CLUSTER MEET 命令,可以让接收命令的节点 A 将另一个节点 B 添加到节点 A 所在的集群中,此时该集群处于下线状态,因为并没有分配hash槽。
3.通过 CLUSTER NODES 命令可以查看集群中的节点信息。
4.CLUSTER ADDSLOTS <slot> [slot ...] :为某个节点设置hash槽的个数
5.CLUSTER KEYSLOT <KEY>命令可以查看 key 属于哪个槽。
节点通过以下算法来定义 key 属于哪个槽:crc16(key,keylen) & 0x3FFF;(0x3FFF = 2^14-1,此处&运算与取模结果相同)
8.2 创建集群后,数据和实例(节点)之间的如何进行映射?
1.给每个实例分配hash槽后,怎么让其他实例知道我的hash槽,怎么让客户端知道每个实例的hash槽?
- Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了,客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
- 当客户端向节点请求键值对时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:
- 如果键所在的槽刚好指派给了当前节点,那么节点会直接执行这个命令;
如果没有指派给当前节点,那么节点会向客户端返回一个 MOVED 错误,然后重定向(redirect)到正确的节点,并再次发送之前待执行的命令。 - 同时,客户端还会更新本地缓存,将该槽与 Redis 实例对应关系更新正确。
2.MOVED
和 ASK
MOVED格式:MOVED : ,slot:键所在的槽,ip:负责处理槽 slot 节点的 ip,port:负责处理槽 slot 节点的 port
-
重新分片出现的情况:
-
- 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
- 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。
- 重新分片可以在线进行,也就是说,重新分片的过程中,集群不需要下线。
-
在重新分片的期间,源节点向目标节点迁移槽的过程中,可能会出现这样一种情况:如果某个槽的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?
在这种迁移部分完成的情况下,客户端就会收到一条
ASK
报错信息。
ASK是当客户端向目标节点发送一个与数据库键有关的命令,并且这个命令要处理的键正好属于被迁移的槽时:
- 源节点会先在自己的数据库里查找指定的键,如果找到的话,直接执行命令;
- 相反,如果源节点没有找到,那么这个键就有可能已经迁移到了目标节点,源节点就会向客户端发送一个
ASK
错误,指引客户端转向目标节点,并再次发送之前要执行的命令。 - ASK 命令的作用只是让客户端能给新实例发送一次请求,而且也不会更新客户端缓存的哈希槽分配信息。而不像
MOVED
命令那样,会更改本地缓存,让后续所有命令都发往新实例。
8.3 复制与故障检测
对于包含 7001
~ 7004
的四个主节点的集群,可以添加两个节点:7005
、7006
。并将这两个节点设置为 7001
的从节点。
CLUSTER REPLICATE <node_id>:设置从节点命令
如果此时,主节点 7001 下线,那么集群中剩余正常工作的主节点将在 7001 的两个从节点中选出一个作为新的主节点。
例如,节点 7005 被选中,那么原来由节点 7001 负责处理的槽会交给节点 7005 处理。而节点 7006 会改为复制新主节点 7005。如果后续 7001 重新上线,那么它将成为 7005 的从节点。如下图所示:
- 集群中每个节点会定期向其他节点发送 PING 消息,来检测对方是否在线。如果接收消息的一方没有在规定时间内返回 PONG 消息,那么接收消息的一方就会被发送方标记为「疑似下线」。
- 集群中的各个节点会通过互相发消息的方式来交换各节点的状态信息。
- 节点的三种状态:在线状态、疑似下线状态 PFAIL、已下线状态 FAIL
一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。在一个集群中,半数以上负责处理槽的主节点都认定了某个主节点下线了,集群才认为该节点需要进行主从切换。 - Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。
8.4 raft算法
故障转移方法和哨兵的很相似,两者都是基于 Raft算法 的领头算法实现的。流程如下:
集群的配置纪元是一个自增计数器,初始值为0;
当集群里的某个节点开始一次故障转移操作时,集群配置纪元加 1;
对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,第一个向主节点要求投票的从节点将获得主节点的投票;
当从节点发现自己复制的主节点进入「已下线」状态时,会向集群广播一条消息,要求收到这条消息,并且具有投票权的主节点为自己投票;
如果一个主节点具有投票权,且尚未投票给其他从节点,那么该主节点会返回一条消息给要求投票的从节点,表示支持从节点成为新的主节点;
每个参与选举的从节点会计算获得了多少主节点的支持;
如果集群中有 N 个具有投票权的主节点,当一个从节点收到的支持票 大于等于 N/2 + 1时,该从节点就会当选为新的主节点;
如果在一个配置纪元里没有从节点收集到足够多的票数,那么集群会进入一个新的配置纪元,并再次进行选主。
九、redis常见问题
缓存雪崩
- 事故现象:
整个缓存系统宕机,所有的请求落到数据库,数据库崩溃,此时若没有特别的方案来处理,重启数据库也是大量请求又进到数据库,立马又给打死。
- 解决方案
//事前:
保证redis集群的高可用性,redis cluster集群架构
数据预热
//事中:
用本地缓存ehcache和hystrix限流组件
ehcache:就是本地的一个小容量的缓存容器
一个请求发送过来之后先走ehcache查询数据,ehcache没有去redis查数据,redis没有再去数据库中查,若是redis整体宕机,则调用限流组件,请求先走ehcache,ehcache没有则进入限流组件,给一部分请求直接返回友情提示或一些默认值,剩余的请求走数据库,要保证数据库不崩溃。
//事后:
redis必须要做持久化,确保redis宕机可以快速恢复
缓存穿透
数据库被恶意攻击,比如一秒钟5000个请求,结果其中4000个请求在数据库和redis都没有数据,查不到数据也存不到缓存里面,下一秒又发送4000个请求找数据库,可能导致数据库崩溃。
//解决
1.每次系统a从数据库只要没查到数据就写一个空值到缓存里面,下次就会返回这个空值。
2.布隆过滤器
缓存击穿
大量请求访问一个key,这个key过期的时候请求就打在了数据库
1.考虑使用锁,当一个请求发现缓存中没有这个key时候,先拿锁来访问数据库得到key
2.延长过期时间或者不设置过期时间
预热
分布式锁
缓存预热:
1、什么是缓存预热:
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
2、缓存预热解决方案:
(1)数据量不大的时候,工程启动的时候进行加载缓存动作;
(2)数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
(3)数据量太大的时候,优先保证热点数据进行提前加载到缓存。
十、保证缓存和数据库的双写一致性
经典的缓存+数据库读写模式cache aside pattern
(1)读的时候先读缓存,没有的话就读数据库,然后取出数据存入redis,同时返回响应
(2)更新的时候先把redis缓存删掉,再更新mysql,然后再删除redis缓存
为什么是删除而不是更新?
可能一个缓存设计很多表,更新缓存代价很高。可能这个数据会被频繁修改而访问量很少,采用懒加载的方式。
数据不一致的几种可能
第一种:
先修改数据库再删除缓存,万一缓存删除失败,那么数据库就和缓存中数据不一致。
解决:
调整一下顺序,先删除缓存再修改数据库,若是删除成功,修改失败,那么还会保证数据库和缓存数据一致。如果删除缓存失败也不会更改数据库。
第二种:
先删除缓存再修改数据库的情况下,在更新一个数据库的时候,同时读取缓存并发的发生了,这个时候还会出现数据库和缓存双写不一致。(删除缓存修改数据库之后,有另外一个线程读取缓存,从数据库拿到数据写入缓存)
解决:
数据库和缓存更新数据进行读写异步串行化
让关于同一数据比如说是商品库存的请求,当访问的数据在缓存中被前一个修改请求删除时,那个让库存服务把这个商品id相关的请求添加到同一个内存队列(根据商品的id进行hash计算,让相同商品id的进入同一内存队列),这个队列对应一个线程,这个线程串行的进行相应的操作,前一个写请求先执行完之后,这个读请求再去数据库查找数据,并存放到缓存中,那么就可以解决这种并发情况下,双写不一致的问题。
优化:
当队列里面有一个读请求时,其余的读请求就没必要进入到队列中了,也就是让一个更新请求后面只跟一个读请求,让其余的请求在那等待一下,不断轮询查询缓存。若是等待超过时间,则直接去读取数据库中的旧值。
若是写请求过高,堆积在队列中,那么会有大量的读请求超时,都打在数据库中,这时候需要做模拟测试,可能需要增加机器,减少每个机器上的写请求数量。
这里还要考虑的是,如何对同一个商品的读写请求,全部路由到同一机器上
可以考虑自己做服务器键的按照某个请求参数的hash路由
或者用nginx的hash路由功能
十一、缓存并发竞争
对于多个客户端,同时更新一个数据,可能出现更新顺序不是我们想要的预期
这时需要zookeeper分布式锁确保同一时间只有一个实例在操作某个key,别人都不允许都和写;
每次写之前都要判断这个value的时间戳是否要比缓存里的value的时间戳要更新,如果更旧就不能用旧的数据覆盖新的数据。
十二、实际应用
redis cluster,10台机器,5台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例,5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。
机器是什么配置?32G内存+8核CPU+1T磁盘,但是分配给redis进程的是10g内存,一般线上生产环境,redis的内存尽量不要超过10g,超过10g可能会有问题。
5台机器对外提供读写,一共有50g内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis从实例会自动变成主实例继续提供读写服务
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100条数据是1mb,10万条数据是1g。常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存的50%。
目前高峰期每秒就是3500左右的请求量
十三、通过redis设计分布式锁
简单模式
SET orderId:1:lock 随机值 NX PX 30000
NX:表示如果原来没有这个key则存储到Redis,并且返回ok,如果原来有这个key,那么设置不成功,返回nil。
PX 30000:过30秒自动删除。
这时多个线程来创建key只有一个能创建成功,其余每隔1秒来尝试设置,只有等获得key的线程释放之后,或者过了30s之后其他线程才能获得锁。
要注意删除key的时候,要先比较redis中的值和你的值是不是一样,如果一样才能删除。
存在问题:若是master宕机,此时这个key还没异步复制到slave上,那么slave变成master,却没有这个数据。
RedLock算法
对redis集群上的master上锁,要求在设定好的时间内在大多数节点上都能创建成功,要求成功的节点数大于等于(n/2+1),如果建立失败则依次删除锁。
只要别人建立了一把分布式锁,你就得不断轮询来创建锁。
Redission
rediss分布式锁无法自动续期,如果在你设置的px里面没有执行完,那么锁就可能被其他线程拿到,就会导致严重的线上问题。
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
十四、redis的批量插入
方案一:
使用Luke协议,通过redis-cli –pipe发送数据到服务器
dis-难点是保证recli在pipe mode模式下执行和netcat一样快的同时,如何能理解服务器发送的最后一个回复。
这是通过以下方式获得:
redis-cli –pipe试着尽可能快的发送数据到服务器。
读取数据的同时,解析它。
一旦没有更多的数据输入,它就会发送一个特殊的ECHO命令,后面跟着20个随机的字符。我们相信可以通过匹配回复相同的20个字符是同一个命令的行为。
一旦这个特殊命令发出,收到的答复就开始匹配这20个字符,当匹配时,就可以成功退出了。
同时,在分析回复的时候,我们会采用计数器的方法计数,以便在最后能够告诉我们大量插入数据的数据量 ,
cat text.txt | redis-cli –pipe
方案二:
采用Jedis的父类中的pipelined()方法获取管道
我们可以采用Jedis的父类中的pipelined()方法获取管道,它可以实现一次性发送多条命令并一次性返回结果,这样就大量的减少了客户端与Redis的通信次数,可以有效的提高程序效率(但是,因为Redis要一次性返回所有结果,它会把这些结果都缓存起来,因此命令越多,缓存消耗的内存也会越大,具体还要视情况而定).此外Pipeline的原理是队列(先进先出),这样也保证了数据的顺序性。
方案三:
使用RedisTemplate批量保存数据
十五、redis的事务
在redis中,事务的作用就是在一个队列中一次性、顺序性、排他性的执行一系列的命令。
事务的生命周期:
1. 事务的创建:使用MULTI开启一个事务
2. 加入队列:在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行
3. EXEC命令进行提交事务
常用的关于事务的命令有:
1. MULTI:使用该命令,标记一个事务块的开始,通常在执行之后会回复OK,(但不一定真的OK),这个时候用户可以输入多个操作来代替逐条操作,redis会将这些操作放入队列中。
2. EXEC:执行这个事务内的所有命令
3. DISCARD:放弃事务,即该事务内的所有命令都将取消
4. WATCH:监控一个或者多个key,如果这些key在提交事务(EXEC)之前被其他用户修改过,那么事务将执行失败,需要重新获取最新数据重头操作(类似于乐观锁)。
5. UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。