前言
随着互联网科技的不断发展,我们以前单纯直接操作数据库的方式已经不能满足现有的高性能和高并发的需求了,于是缓存技术应用而生。
![fb1f1618df9995cc0eb3718508f9a09d.png](https://i-blog.csdnimg.cn/blog_migrate/102672dce9f17a115faf57c98cd36ea7.jpeg)
当前比较成熟的缓存技术有:MongoDB、Redis、Memcache,那么此文主要讲的是其中的Redis,也许我们在平常的工作中也用到过,但大多数也仅限于简单的使用了,可能很多的知识点我们并不知道,那么就跟我重新来认识一下它吧。
Redis定义
Redis全称:Remote Dictionary Server,本质上是一个 Key-Value 类型的内存数据库。
Redis的特点
支持丰富的数据类型(string、hash、list、set,sorted set等等)、支持数据磁盘持久化存储、支持主从、支持分片。
为什么redis效率高,速度快?
1、redis是完全基于内存的操作,不会受到磁盘IO的限制。
2、redis是单线程操作,可以避免频繁的上下文切换。
3、redis采用了非阻塞I/O多路复用机制,单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。
Redis高级用法
假设redis中有十亿个Key,如何从这么多Key中找到固定前缀的Key?
我们一般的方法可能就是直接使用Keys [pattern]来查找所有符合给定模式Pattern的Key,即keys test* //返回所有以test为前缀的key,但是当面对海量数据时,这个操作会对内存造成很大的消耗,因此我们可以用更高级的操作:使用SCAN cursor [MATCH pattern] [COUNT count],即SCAN 0 MATCH test* COUNT 10 //每次返回10条以test为前缀的key。
如何使用redis实现分布式锁?
从 Redis 2.6.12 版本开始,我们就可以使用 Set 操作,将 SETNX 和 EXPIRE 融合在一起执行:SET KEY value [EX seconds] [PX milliseconds] [NX|XX]
代码如下:
RedisService redisService = SpringUtils.getBean(RedisService.class);
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if("OK.equals(result)"){
doOcuppiredWork();
}
Redis持久化
持久化,即将数据持久存储,而不因断电或其他各种复杂外部环境影响数据的完整性。
Redis 目前有两种持久化方式,即 RDB 和 AOF,但是两者都有优缺点:
RDB 优点:全量数据快照,文件小,恢复快。
RDB 缺点:无法保存最近一次快照之后的数据。
AOF 优点:可读性高,适合保存增量数据,数据不易丢失。
AOF 缺点:文件体积大,恢复时间长。
因此,Redis 4.0 之后推出了此种持久化方式,RDB 作为全量备份,AOF 作为增量备份,并且将此种方式作为默认方式使用。在 RDB-AOF 方式下,持久化策略首先将缓存中数据以 RDB 方式全量写入文件,再将写入后新增的数据以 AOF 的方式追加在 RDB 数据的后面,在下一次做 RDB 持久化的时候将 AOF 的数据重新以 RDB 的形式写入文件。这种方式既可以提高读写和恢复效率,也可以减少文件大小,同时可以保证数据的完整性。
RDB 和 AOF 文件共存情况下的恢复流程如下图:
![a7da82600b2912a370d835ad216bf9f8.png](https://i-blog.csdnimg.cn/blog_migrate/4d8d9cbb7c50f98ad3e84c49d79e20c7.jpeg)
Redis的同步机制
1、主从同步模式(全同步和增量同步):Redis 一般是使用一个 Master 节点来进行写操作,而若干个 Slave 节点进行读操作,另外定期的数据备份操作也是单独选择一个Slave去完成,这样可以最大程度发挥Redis的性能,为的是保证数据的弱一致性和最终一致性,但是也存在一定的弊端,当Master宕机后,Redis集群将不能对外提供写入操作。
2、Redis Sentinel(哨兵):解决主从同步 Master 宕机后的主从切换问题,但没有解决Master写的压力,哨兵主要有以下特点:
1)监控:检查主从服务器是否运行正常。
2)提醒:通过 API 向管理员或者其它应用程序发送故障通知。
3)自动故障迁移:主从切换(在 Master 宕机后,将其中一个 Slave 转为 Master,其他的 Slave 从该节点同步数据)。
Redis 集群方案
1、twemproxy,大概概念是,它类似于一个代理方式, 使用时在本需要连接 redis 的地方改为连接 twemproxy, 它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 redis,将结果再返回 twemproxy。
缺点: twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
2、codis, 目前用的最多的集群方案, 基本和 twemproxy 一致的效果, 但它支持在节点数量改变情况下,旧节点数据可恢复到新 hash 节点。
3、Redis cluster3.0 自带的集群, 特点在于他的分布式算法不是一致性 hash, 而是 hash槽的概念, 以及自身支持节点设置从节点,具体看官方文档介绍。
Redis的过期策略以及内存淘汰机制
Redis采用的是定期删除+惰性删除策略。
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是惰性删除派上用场,也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了,如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高,那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
# maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的,例举几种。
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(推荐使用,目前项目在用这种)
3)volatile-lru:尝试回收最少使用的键, 但仅限于在过期集合的键,使得新添加的数据有空间存放。
4)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
5)volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
6)volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL) 较短的键,使得新添加的数据有空间存放。
Redis和数据库双写一致性问题
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。如果对数据有强一致性要求,不能放缓存。另外采取的措施是,首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。
如何应对缓存穿透、缓存击穿和缓存雪崩问题
缓存穿透:即故意去查询缓存不存在的key,导致所有的请求都怼到数据库上,从而导致数据库压力倍增。
解决方案:对查询结果为空的情况也进行缓存(再次访问直接返回空);提供一个能迅速判断请求是否有效的拦截机制(如:布隆过滤器)
缓存击穿:即故意同一时间去查询缓存中不存在的同一个key,导致所有请求都怼到数据库上,从而导致数据库连接异常。
解决方案:设置或自动检测热点Key(加大key的过期时间或设置为永不过期);加互斥锁(只允许有锁的操作去访问数据库,然后更新缓存,其它操作可从缓存取值)。
缓存雪崩:即缓存同一时间大面积的key失效,导致所有请求都怼到数据库上,从而导致数据库连接异常。
解决方案:让Key的失效时间分散开,可以在统一的失效时间上再加一个随机值,或者使用更高级的算法分散失效时间;构建多个redis实例,个别节点挂了还有别的可以用;多级缓存(比如增加本地缓存,减小redis压力);对存储层增加限流措施,当请求超出限制,提供降级服务(一般就是返回错误即可)。
如何解决Redis的并发竞争key问题
如果对这个key操作,不要求顺序,则准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可;如果对这个key操作,要求顺序,这种时候我们在数据写入数据库的时候,需要保存一个时间戳,时间早的就不做set操作了。
Redis为什么采用跳表而不是红黑树?
1、在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
2、平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
3、从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
4、查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
5、从算法实现难度上来比较,skiplist比平衡树要简单得多。
什么是HyperLogLog?
HyperLogLog 是一种概率数据结构,用来估算数据的基数。基数就是指一个集合中不同值的数目,比如 a, b, c, d 的基数就是 4,a, b, c, d, a 的基数还是 4。虽然 a 出现两次,只会被计算一次。
Redis 的 HyperLogLog 通过牺牲准确率来减少内存空间的消耗,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。所以 HyperLogLog 是否适合在比如统计日活月活此类的对精度要求不高的场景。
以上只是对Redis的一些基本概念和知识点做一下说明,最好是能针对自己所从事的语言去动手操作一番,实践出真知,纸上谈兵往往还是心里没底。
参考链接:
https://www.toutiao.com/i6747926805744730627/
https://www.toutiao.com/i6749708288830472716/