《成长之路 - Redis系列》 - (二)Redis进阶

前言

 上一篇介绍了redis基础类型,持久化机制、同步机制以及redis高可用大概原理。
 传送门:《成长之路 - Redis系列》 - (一)Redis基础

几种缓存问题

缓存雪崩:

 缓存雪崩是指key值大量失效或者redis节点挂掉,这时有大量请求直接打到了数据库,导致数据库扛不住直接挂掉。
应对:
1.针对key值大量失效可以对key的过期时间加上随机值,这样key就不会再同一时间过期了。
2.对于redis节点挂掉。我们可以设置使用Redis Sentinel 和 Redis Cluster 保证redis高可用。

缓存击穿:

 缓存击穿是指非常热点的数据突然过期,这时大量的请求打到了数据库。比如秒杀前商品的数据过期,到了秒杀的时候请求全打到数据库了。
应对:
1.对于非常热点的数据可以设置永不过期。这里有两种方案:一是本意上的不设置过期时间;二是将过去时间放在value里,当发现快过期时异步刷新。
2.采用互斥锁。当在缓存获取不到数据时才去数据库查,然后将查询结果写入缓存,大量并发的时候会有多个线程读数据库写缓存,所以我们只要加个锁只让一个线程去数据库读就好了,其它线程等缓存写好了读缓存就行。

缓存穿透:

 缓存穿透是指请求的数据在缓存中没有,在数据库里也没有,用户不断的发起请求都会直接打到数据库中。这种情况一般都是恶意请求,就是为了搞崩数据库的。
应对:
1.做校验,不合法的直接返回,比如id小于0。
2.即使数据库查不到数据,也要在缓存中保存,设置一个短点的过期时间就好。
3.布隆过滤器。这里简单介绍下布隆过滤器好了:定义一个足够长的数组,因为我们只要知道数据存不存在就行,所以我们可以往数组存byte类型节省空间,其次就是采用多个散列函数,将每个hash值映射到数组中。对于一个数据,我们计算完hash,只要数组对应的值都是1就可以代表这个数据存在了。但是布隆过滤器会有误判,它只能保证判断出不存在的数据一定是不存在的,但是判断出存在的数据可能不存在。

内存淘汰机制:

 当数据过期时redis有两种方式删除过期数据:一是定期删除,就是默认100ms随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了;二是惰性删除,就是我不主动删,在查询的时候检查是不是过期了,过期了就删,没过期返回。

为啥不在定期的时候把所有过期的数据都删了呢?那是因为这样的话要遍历整个数据库,很影响性能。
那定期惰性都没删除的呢?内存淘汰机制。

redis一共有6种淘汰机制:
1.volatile-random: 在设置过期的数据里面随机回收。
2.volatile-lru: 在设置过期的数据里面回收最近最少使用(LRU)的。
3.**allkeys-random:**在所有数据里面随机回收。
4.**allkeys-lru:**在所有数据里面回收最近最少使用(LRU)的。
5.**volatile-ttl:**在所有数据里回收存活时间(TTL)最短的。
6.**noeviction:**返回错误并申请内存。

分布式锁:

 redis是可以连接多个系统的,当多个系统操作相同的数据,就会出现数据不一致的情况,所以我们需要分布式锁,就像是多线程中的synchronized一样。

基本实现思路:

1.采用setnx key value命令,key不存在就会创建key值,key存在就会等待,这样就可以保证同一时间只有1个系统获取key进行操作,完成操作后使用 del key 删除键,就相当于释放锁了。

2.为了避免忘记释放锁可以使用expire key seconds 指令设置过期时间;

3.setnx 和expire是两条命令,所以会有setnx成功expire失败的情况,所以redis2.8版本之后引入了指令 set key value [],该指令可以同时执行 setnx 和 expire ,解决了死锁问题;

4.还有一种情况是系统A获取到锁没完成操作前,expire时间到了自动释放了锁,这时B系统获取锁,当系统A完成后就把锁又给释放了,第三台又能获取锁了,这样就会出现两个系统操作1个key的情形。这种情况可以在每个系统获取锁的时候加上一个标志,释放锁的时候标志不对就不能释放。

还可以基于zookeeper实现分布式锁:

1.zookeeper节点有4种,临时性节点、临时性顺序节点、持久节点、持久性顺序节点,而且它们都具有唯一性,创建后就不能再次创建了,zookeeper还有一个监听机制,客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)等,zookeeper会通知客户端。我们就可以利用这一点实现分布式锁。
2.系统A创建/lock节点(create /lock),创建成功代表加锁成功了,这时系统B再创建会失败,它会监听这个节点的变化,等到系统A删除节点后它就能创建了。
3.当有100个系统尝试加锁时,只有一个会加锁成功,剩下的99个都会监听等待。如果加锁成功后系统挂掉就没法删除锁了,那么剩下的系统就会一直等通知。这种情况可以用zookeeper的临时性节点,这种节点当系统断开连接时会自动删除节点。
4.惊群效应。当一个节点发生变化时会通知剩下的所有节点,但是只有一个会创建成功。为了处理这种情况可以使用临时性顺序节点,这种节点时在创建时zooKeeper会自动给它分配编号比如0000001。这样针对100个系统,可以创建100个节点,/lock0000001、/lock0000002等,每个节点监听它前面的。001处理完删除节点002会收到通知,002删除003会收到通知。

与memcache区别:

1.memcache只支持简单的KV,redis有5种基础数据类型。
2.memcache不支持持久化,而redis支持,所以在存储数据安全和灾难恢复方面redis更可靠。
3.memcache可以使用多核,而redis单线程只能单核,在存储大数据(指单文件大)时memcache性能更高。
4.memcache需要依靠客户端实现集群,redis原生就支持集群。

redis单线程能保证高性能原因:
1.纯内存操作。
2.采用IO多路复用模型。(注意IO多路复用是异步堵塞模型,对IO操作来说是堵塞的,但对socket来说是非堵塞)
3.redis单线程反而避免了多线程间频繁的上下文切换问题。

高并发与高可用:

单机的redis只能支持几万的QPS,在面对秒杀这种场景是远远不够的。我们可以采用读写分离的集群来应对,1个主节点master对应多个从节点slaves,master负责写入,通过异步形式到slave。redis的主从同步上节已经说过了,这里有个问题就是master要不要做持久化,网上的博客分为两种:
1.第一种master必须做持久化,因为当master宕机或重启后数据集是空的,这时将空的数据集分发到slave中那么整个集群就全为空了。
2.第二种master不要做持久化。考虑到性能问题,当master做持久化生成的AOF很大,当加载AOF时会出现短暂暂停服务,这会很影响用户体验。对于重要的数据用slave做持久化。
那到底哪个对呢?我咨询了一个大佬,他们公司采用的是后者,master什么都不开,一些slave开RDB,一些开AOF。这也并不是说第一种做法不对,肯定也是有相应的处理方案的。那对于master的宕机重启问题呢?redis Sentinel宕机后切换主节点,但是sentinel还没选举出新master它重启了怎么办,那就可以通过管家zookeeper进行控制,上面提到的临时性节点,master断开后通知zookeeper,让它即使马上重启也不让它当主节点就好了。接下来就是sentinel选举新的master了。

redis sentinel:
sentinel(哨兵)是redis集群架构中非常重要的一个组件,主要功能如下:

(1)集群监控,负责监控redis master和slave进程是否正常工作。
(2)消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
(3)故障转移,如果master node挂掉了,会自动转移到slave node上。
(4)配置中心,如果故障转移发生了,通知client客户端新的master地址。

哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

(1)故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
(2)即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

	哨兵集群至少要有3个节点,当只有两个节点时会发生问题:master节点挂掉后,只要1个哨兵认为master
	挂了就可以进行切换,同时两个哨兵会选举出一个来执行故障转移,这样是没问题的,但是如果master整个
	机器挂掉了,那么就剩下一个哨兵了,这时就没办法执行选举处理故障转移了。
redis cluster:
上节说到redis cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。采用cluster
架构还可以达到负载均衡的目的:

1.提到负载均衡我们很容易想到一致性哈希算法,通过环形的hash值空间与虚拟节点能很好的实现负载均衡,但是redis cluster并没有采用一致性哈希算法,那我为啥要提它?就是单纯的想提下~ 😦
2.redis cluster采用了hash slot算法,它固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。增加一台节点只要从别的节点中分一些slot过去就好了,同样减少节点就把节点的slot分给其它节点。请求到达时,通过key得到对应的slot,然后就能得到节点了。
3.redis cluster也有类似sentinel的选举功能,不光能实现扩展性,也能实现高可用。所以可以优先选择redis cluster

二者扩展性区别:

1.reids sentinel主从读写分离,只有一个主节点连接了多个从节点,提高读能力只要增加从节点就好了,相当于下图中五角星增加一个角。
2.redis cluster有多个主节点,每个主节点下对应多个从节点,主节点与从节点数据相同,但是各个主节点中的数据不同,提高读能力要增加主节点,相当于下图中增加一个四角星。
在这里插入图片描述

一致性问题:

只要用到缓存就会涉及到缓存与数据库一致性问题,虽然有些情况下允许一段时间内的不一致,但如果处理不好的话会导致缓存与数据库一直不一致(脏数据)。有的系统比如银行要严格要求数据库与缓存要保持一致,这种情况把读请求和写请求串行化,串到一个内存队列里去。串行化是指写请求先删除缓存然后将请求路由放到一个队列里顺序执行,当读请求到来时,如果缓存没有数据,那么已将它路由放到队列里,这样就能保证读请求一定在写请求之后了。采用串行化可以保证一定不会出现数据不一致的情况,但是会大大削减了系统的吞吐量。一般情况是不会使用串行化的,一致性问题还有以下解决方案:

1.先更新数据库后删除缓存:

这是最经典的方式,也是用的最多的。查询请求,如果缓存不存在则查询数据库,查到了就把结果写入缓存。更新的时候先到数据库里删除缓存,然后将缓存删除。这里是删除缓存而不是更新缓存是因为有的更新完成后很久都不会再查询,那就白白浪费了内存空间,说白了也就是懒汉的思想。还有一个原因是很多时候缓存中的数据比较复杂,往往是几个表联合查询得到的,只更新一个表就去更新缓存还要再次查询数据库,代价太大了。
问题:
1.在更新数据库期间读到的缓存是旧的,这个其实影响很小,基本可以忽略。
2.来了一个读请求没有命中缓存,然后去查询数据库,这时来到一个写请求,写完成后删除缓存,最后一开始的读请求完成写入缓存,这时缓存就是脏数据。这种情况理论上会出现,但是实际上发生的概率很低,因为写请求本来就比读请求慢,这里还要读操作在写操作前进入数据库,还要在写操作后删除缓存完后再完成。

2.先删除缓存再更新数据库:

问题:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
这种线程安全问题需要通过延时双删等方案解决,大概的策略是:
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠x秒,再次淘汰缓存

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值