目录
4.3.3 操作缓存的时候,到底是删除缓存呢,还是更新缓存?
1. 我是谁,我从哪里来,将到哪里去
万事万物追根揭底都是一个哲学问题,本篇文章就从redis的前世今生讲起,希望给大家学习redis带来一些帮助。
1.1 我是谁
Redis 是互联网技术领域使用最为广泛的存储中间件,它是“Remote Dictionary Service ” (远程字典服务〉的首字母缩写。
1.1.1 特点:
>> Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
>> Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
>> Redis支持数据的备份,即master-slave模式的数据备份。
1.1.2 优势:
>> 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
>> 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
>> 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。
>> 单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
1.2 我从哪里来
Redis 由意大利人 Salvatore Sanfilippo (网名 Antirez )开发,图 1-1 所示的是他 的个人照片。
我们都知道 Redis 的默认端口是 6379 ,这个端口号也不是随机选的,而是由于 机键盘字母“MERZ ”的位置决定的,如图1-2 所示。
“MERZ ”在 Antirez 的朋友圈语言中是“愚蠢”的代名词,它由于意大利广告 女郎“Alessia Merz "在电视节目上说了一堆愚蠢的话
而被人熟知.
1.3 将到哪里去
中高级后端开发者绕不开的必备技能
提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,提高效率,将数据缓存
在内存中。
2. 下载安装
为你,千千万万遍
请移步百度Google一下,或Google百度一下。
3. “集美”们
人间烟火,山河远阔;无一是你,无一不是你。
(1) 5 种基础数据结构
<1>. string( 字符串):Redis最基本的数据类型,一个键对应一个值,一个键值最大存储512MB。
<2>. list (列表):hash是一个键值对的集合,是一个String类型的field和value的映射表,适合用于存储对象。
<3>. hash (字典):是redis的简单的字符串列表,按插入顺序排序。
<4>. set (集合):是String字符串类型的无序集合,也不可重复 。
<5>. zset (sorted set 有序集合):是String类型的有序集合,也不可重复。
有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。
(2) 其他的数据结构
<1>. 位图:最小单位是比特(bit ),每个 bit 的取值只能是 0或1,其实就是普通的字符串,也就是 byte 数组。
可以用于统计用户一共签到了多少天。
<2>. Hyperloglog:用来解决这种统计问题的,提供不精确的去重计数方案,虽然不精确,但是也不是非常离谱,
标准误差是 81%,主要用于不精确的UV统计。
<3>. .......
(3) 基本语法
(4)通用实例
(5)通用基本命令
请参考:https://www.runoob.com/redis/redis-keys.html
3.1 集美之String(字符串)
>> Redis 最简单的数据结构,如图 1-4 所示,它的内部表示就是一个字符数组。
>> 字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。
>> Redis 的字符串是动态字符串,是可以修改的字符串,内部结构的实现类似于Java的 ArrayList ,
采用预分配冗余空间的方式来减少内存的频繁分配,如图 1-4 所示。
注:常用命令请参考:https://www.runoob.com/redis/redis-strings.html
3.2 集美之List (列表)
>> 相当于 Java 语言里面的 LinkedList 注意它是链表而不是数组。
>> 如图 1-5 所示 表中的每个元素都使用双向指针顺序,串起来可以同时支持前向后向遍历。
>> 本结构常用来做异步队列使用 将需要延后处理的任务结构体序列化成字符串,塞进 Redis 的列表,
另个线程从这个列表中轮询数据进行处理。
注:常用命令请参考:https://www.runoob.com/redis/redis-lists.html
3.3 集美之Hash (字典)
>> Redis 的字典相 当于 Java 语言里面的HashMap ,如 1-7 示,它是无序字典内部存储了很多键值对。
>> 实现结构上与Java的HashMap 是一样的,都是“数组+链表”二维结构。如图 1-8 所示,第一维hash 的数组位置碰撞时,
就会将碰撞的元素使用链表串接起来。
注:常用命令请参考:https://www.runoob.com/redis/redis-hashes.html
3.4 集美之Set (集合)
>> 集合相当于 Java 语言里面的 HashSet ,它内部的键值对是无序的、唯一的。它的内部实现相当于一个特殊的字典,
字典中所有的 value 都是一个值 NULL。
>> set 结构可以用来存储在某活动中中奖的用户 ID ,因为有去重功能,可以保证同一个用户不会中奖两次。
注:常用命令请参考:https://www.runoob.com/redis/redis-sets.html
3.5 集美之Zset (sorted set 有序集合)
>> zset 可能是 Redis 提供的最有特色的数据结构。如图 1-10 所示,它类似于Java的SortedSet和HashMap的结合体,
一方面它是个 set ,保证了内部 value 的唯性,另方面它可以给每个 value 赋予一个 score , 代表这个 value 的排序权重。
>> 它的内部实现用的是一种叫作“跳跃列表”的数据结构。
>> zset 可以用来存储粉丝列表, value 值是粉丝的用户 ID, score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
>> zset 还可以用来存储学生的成绩, value 值是学生的 ID, score 是他的考试成绩。我们对成绩按分数进行排序就可以得到
他的名次。
注:常用命令请参考:https://www.runoob.com/redis/redis-sorted-sets.html
4. 什么是快乐星球
什么是快乐星球,如果你想知道什么是快乐星球的话,那么我们一起来研究研究。
4.1 快乐星球之应用
(1) 持久化数据:利用zset类型存储排行榜、利用hash记录点赞数、评论数等
(2) 高速缓存:会话缓存、热点数据缓存
(3) 分布式锁
(4) 消息队列
(5) 自增序列:活动排行榜、计数、分布式id
(6) 发布,订阅消息:消息通知
4.2 快乐星球之分布式锁
4.2.1 常见的分布式锁
(1) MySql : 建一张分布式锁的表实现。
(2) Zk :请移步去学习Zk相关知识。
(3) Redis :请继续阅读本篇博客。
(4) 自研分布式锁:如谷歌的Chubby。
4.2.2 Redis分布式锁实现
(1) 简单实现:
<1> 对于某个资源加锁:setNx resourceName value
<2> 加入过期时间:set resourceName value ex 5 nx
(2) Redission: Redission是Redis的客户端,相比于Jedis功能简单。
(3) RedLock:利用多个Redis集群,用多数的集群加锁成功,减少Redis某个集群出故障, 造成分布式锁出现问题的概率。
4.3 快乐星球之缓存一致性
4.3.1 谈谈一致性
(1) 强一致性:要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
(2) 弱一致性:系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地
保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
(3) 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。
4.3.2 三个经典的缓存模式
(1) Cache-Aside Pattern:旁路缓存模式。
(2) Read-Through/Write-through:Read-Through实际只是在Cache-Aside之上进行了一层封装,它会让程序代码变得更简洁,
同时也减少数据源上的负载。
(3) Write-behind(异步缓存写入):通过批量异步的方式来更新数据库。
4.3.3 操作缓存的时候,到底是删除缓存呢,还是更新缓存?
>> 日常开发中,我们一般使用的就是Cache-Aside模式。
Cache-Aside在写入请求的时候,为什么是删除缓存而不是更新缓存呢?
1.线程A先发起一个写操作,第一步先更新数据库。
2.线程B再发起一个写操作,第二步更新了数据库
3.由于网络等原因,线程B先更新了缓存
4.线程A更新缓存。
这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。
如果是删除缓存取代更新缓存则不会出现这个脏数据问题。
4.3.4 双写的情况下,先操作数据库还是先操作缓存?
>> Cache-Aside缓存模式中,有些小伙伴还是会有疑问,在写请求过来的时候,为什么是先操作数据库呢?
为什么不先操作缓存呢?
1.线程A发起一个写操作,第一步del cache
2.此时线程B发起一个读操作,cache miss
3.线程B继续读DB,读出来一个老数据
4.然后线程B把老数据设置入cache
5.线程A写入DB最新的数据
酱紫就有问题啦,缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。
因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。
5. 爷青回
世间所有的相遇都是久别重逢
5.1 爷青回之跳表
跳表是一种数据结构。它允许快速查询一个有序连续元素的数据链表。
跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n)。
跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。
首先其位于 L0 层的概率肯定是 100% ,而兼职到 Ll 层只有 50% 的概率,到L2 层只有 25% 概率, 到 L3 层只有 12.5% 的概率,以此类推, 直随机到最顶层 L31(最多32层)绝大多数元素都过不了几层, 只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,元素能进入到顶层的可能 性就会越大。
5.1.1 构建一个跳表
5.1.2 删除节点
5.1.3 查
>> 查找 5 的过程:
1.head->8, 8>5,从head开始,去下一层查找。
2.head->4->8, 8>5,从 4 元素开始查找。去下一层查找
3.head->4->8, 8>5,从 4 元素开始查找。去下一层查找.
4.head->4->6, 6>5,从 4 元素开始查找。去下一层查找.
5.head->4->5, 5==5,返回节点5.
5.1.4 跳表 与平衡树、哈希表的比较
注:p:指针数目
5.1.5 Redis中作者选择SkipList的原因
5.2 爷青回之淘汰策略
5.2.1 过期删除策略
删除达到过期时间的key。
1.定时删除
>> 对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。
>> 该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去 处理过期的数据,
会影响Redis的吞吐量和响应时间。
2.惰性删除
>> 当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,
但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,
因此不会被清除,导致占用了大量的内存。
3.定期删除
>> 每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,
还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。
5.2.2 内存淘汰策略
Redis的内存淘汰策略,是指内存达到maxmemory极限时,使用某种算法来决定清理掉哪些数据,以保证新数据的存入。
5.2.3 Redis的内存淘汰机制
1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。
4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key。
5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
5.2.4 LFU 模式
Redis 4.0 里引人了一个新的淘汰策略-----LFU 模式
注:
>> LRU的全称是Least Recently Used
>> LFU 的全称是 Least Frequently Used
>> 在配置文件中,通过maxmemory-policy可以配置要使用哪一个淘汰机制。
5.3 爷青回之持久化
5.3.1 快照(RDB:Redis DataBase)
快照原理:操作系统的多进程 COW (Copy On Write )机制
注:ROW(Redirect on first write) ,实例:mysql的binlog
5.3.2 AOF (Apend Only File)日志
AOF原理:存储的是 Redis 服务器的顺序指令序列, AOF 日志只记录对内存进行修改的指令记录。
>> AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际 上是将内容写到了内核为文件描述符分配的
一个内存缓存中,然后内核会异步将脏 数据刷回到磁盘的。
>> 在生产环境的服务器中, Redis 通常是每隔 ls 左右执行一次 fsync 操作,这 ls 的周期是可以配置的。
这是在数据安全性和性能之间做的一个折中,在保持高 性能的同时,尽可能使数据少丢失。
>> 其他类似技术:log4j
6. 年轻人不讲武德
所有命运的馈赠,都早已暗地里标好了价格。
6.1 年轻人不讲武德之雪崩
6.1.1 现象与原因
1.查询变慢,大面积服务不可用。
2.同一时间缓存大面积失效,所有的请求直接打到数据库上,DB扛不住挂了,如果是重要的库,
例如用户库,那牵联就一大片了,瞬间倒一片。
6.1.2 解决方案
1. 批量往redis存数据的时候,把每个key的失效时间加上个随机数,这样就能保证数据不会在同一个时间大面积失效。
6.2 年轻人不讲武德之击穿
6.2.1 现象与原因
1.与雪崩类似,但是又不一样。雪崩是因为大面积缓存失效,请求全打到DB;
而击穿是指一个key是热点,并发请求全都集中访问此key,而当此key过期瞬间,持续的并发就击穿缓存,全都打在DB上。
进而又引发雪崩的问题。
6.2.2 解决方案
1.设置热点key不过期。或者加上互斥锁。
6.3 年轻人不讲武德之穿透
6.3.1 现象与原因
1.指用户不断发起请求的数据,在缓存和DB中都没有,比如DB中的用户ID是自增的,但是用户请求传了-1,
这个时候用户很有可能就是一个攻击者,这样的功击会导致DB的压力过大,严重的话就是把DB搞挂了。
因为每次都绕开了缓存直接查询DB
6.3.2 解决方案
1.在接口层增加校验,不合法的参数直接返回。不相信任务调用方,根据自己提供的API接口规范来,
作为被调用方,要考虑可能任何的参数传值。
2.在缓存查不到,DB中也没有的情况,可以将对应的key的value写为null,或者其他特殊值写入缓存,
同时将过期失效时间设置短一点,以免影响正常情况。这样是可以防止反复用同一个ID来暴力攻击。
3.正常用户是不会这样暴力功击,只有是恶意者才会这样做,可以在网关NG作一个配置项,为每一个IP设置访问阀值。
4.高级用户布隆过滤器(Bloom Filter),这个也能很好地防止穿透。原理就是利用高效的数据结构和算法快速判断出
你这个Key是否在DB中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。
7. yyds
你有没有很想和谁重新认识一次
7.1 yyds之多线程
>> Redis 4.0版本引入了Lazy Free,将慢操作异步化,这也是在事件处理上向多线程迈进了一步。
>> Redis 6.0版本实现了多线程I/O:
正如官方以前的回复,Redis的性能瓶颈并不在CPU上,而是在内存和网络上。
因此6.0发布的多线程并未将事件处理改成多线程,而是在I/O上,此外,如果把事件处理改成多线程,不但会导致锁竞争,
而且会有频繁的上下文切换,即使用分段锁来减少竞争,对Redis内核也会有较大改动,性能也不一定有明显提升。
>> 局限性:Redis6.0版本的多线程并非彻底的多线程,I/O线程只能同时执行读或者同时执行写操作,
期间事件处理线程一直处于等待状态,并非流水线模型,有很多轮训等待开销。
>> 小结:Redis 4.0引入Lazy Free线程,解决了诸如大键删除导致服务器阻塞问题,在6.0版本引入了I/O Thread线程,
正式实现了多线程,但相较于Tair,并不太优雅,而且性能提升上并不多,压测看,
多线程版本性能是单线程版本的2倍,Tair多线程版本则是单线程版本的3倍。
8. 最后的晚餐
本文主要介绍的Redis相关重点内容,有详有略,如某部分想深入学习可以去Redis官网或通过其他途径学习,因水平有限,文中所述难免有不当之处,欢迎批评指正,如想交流学习,也欢迎读者在评论处留下自己的见解和想法。