目录
一、Redis 概述
1.Redis介绍
- Redis是一个开源的key-value存储系统
- 和MemCached 类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(有序集合)、hash(哈希)。
- 这些数据类型都支持push/pop、add/remove 及取交集和差集及更丰富的操作,而这些操作都是原子性的。
- 和memcached一样,为了保证效率,数据都是缓存在内存中的。
- 区别是redis会周期性的将更新的数据写入磁盘或将修改的操作写入追加的记录文件。
- 并且在此基础上实现了master-slave主从同步。
2.应用场景
- 配合关系数据库做高速缓存
- 分布式session共享
- 多样的数据结构存储持久化数据。
3.相关技术
Redis 使用的是单线程+多路IO复用技术:
多路复用是指使用一个线程来检查多个文件描述符的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞知道超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程池执行。
二、Redis 数据类型
Redis 字符串(string)
1.概述
- String 是Redis 最基本的数据类型,你可以理解成与MemCached一摸一样的类型,一个Key对应一个Value。
- String类型是二进制安全的,意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
- String类型是Redis最基本的数据类型,一个Redis中字符串最大长度512M。
2.数据结构
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS),是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有空间,如果超过1M,扩容时一次只会多扩1M的空间。字符串最大长度为512M。
Redis 列表(List)
1.概述
- 单键多值:Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(坐标)或者尾部(右边)。
- 它的底层是双向链表,对两端的操作性能很高,通过索引下标来操作中间的结点性能会较低。
2.数据结构
- List的数据结构为快速链表quickList。
- 首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构叫ziplist(压缩列表)。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
- 当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针pre + next。
- Redis将链表和ziplist结合起来组成了quickList。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入、删除性能,又不会出现空间的冗余。
Redis 集合(set)
1.概述
- Redis set 对外提供的功能与list类似,是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复的数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
- Redis的Set 是string类型的无序集合。它的底层其实是一个value为null的hash表,所以添加、删除、查找的时间复杂度为O(1).
- 一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查询数据的时间不变。
2.数据结构
- Set的数据结构是Hash.
- Java中HashSet的内部实现使用的是HashMap,只不过所有的Value都执行同一个对象。Redis的Set结构也是一样的,它的内部也是使用Hash结构,所有的Value都指向同一个内布值。
Redis哈希(Hash)
1.概述
- Redis hash是一个键值对集合。
- Redis hash是一个string类型的field和value的映射表,hash特别适合于存储对象。
- 类似于Java里面的Map<string,object>.
- 用户ID为查找的Key,存储的Value为用户对象,包括姓名、年龄、生日等信息,如果用普通的key/value 结构来存储,主要有一下2中存储方式。
方法一:每次修改用户的某个属性都需要反序列化,改好之后,再序列化回去。开销较大。
方法二: 用户ID数据冗余。
通过Key(用户ID)+Field 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
2.数据结构
- Hash类型对应的数据结构有两种:ZipList 、HashTable 。当field-value长度较短个数较少时,使用ziplist、否则使用Hashtable。
Redis 有序集合Zset (sorted set)
1.概述
- Redis有序集合zset和普通集合Set非常类似,是一个没有重复元素的字符串集合。
- 不同之处是有序集合的每个成员都关联了一个评分,这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,这个评分可以重复的。
- 因为元素是有序的,所以你也可以很快的根据评分(socre)或者次序(position)来获取一个范围的元素。
- 访问有序集合的中间元素也是非常快的,因为你能够使用有序集合作为一个没有重复成员的智能列表。
2.数据结构
SortedSet 是redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构,Map<string,double> ,可以给每一个元素Value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重Score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
zset底层使用了两个数据结构:
- hash的作用就是关联元素value和score,保障元素value的唯一性,可以通过元素value找到相应的score值。
- 跳跃表,跳表的目的在于给元素排序,根据scoer的范围查找元素。
3.跳跃表
3.1 简介
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数据不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。redis采用的是跳表,跳表的效率堪比红黑树,实现远比红黑树简单。
3.2 实例
对比有序链表和跳跃表,从链表中查询出51:
要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。
- 从第二层开始,1结点比51结点小,向后比较;
- 21结点比51结点小,继续向后比较,后面就是NULL了,所以从21结点向下到第一层。
- 在第一层,41结点比51小,继续向后,61结点比51结点大,所以41向下。
- 在第零层,51结点要为查找的结点,结点找到,共查找4次。
由此可以看出,跳表比有序链表效率更高。
三、Redis Bitmaps
1.概述
Redis 提供了Bitmaps 这个数据类型,可以实现对位的操作。
- Bitmaps 本身不是一种数据类型,实际上它就是字符串,但是它可以对字符串的为进行操作。
- Bitmaps 单独提供了一套命令,所以在Redis 中使用Bitmaps 和使用字符串的方法不太相同。可以把Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下表在Bitmaps 中叫做偏移量
2.Bitmaps 与 Set对比
假设网站有1亿用户,每天独立访问的用户有5千万,如果每天集合类型和Bitmaps 分别存储活跃用户可以得到表:
很显然,这种情况下使用Bitmaps 能节省很多的内存空间,尤其是随着时间推移节省的内存还是非常可观的。
但Bitmaps 并不是万金油,假如该网站每天的独立访问用户很少,假如只有10万,那么两者的对比如下表所示,很显然,这时候使用Bitmaps 就不合适了,因为基本上大部分位都是0。
四、HyperLogLog
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(pageview),可以使用Redis 的incr、incrby轻松实现。但像UV(uniqueVistor独立访客)、独立IP数、搜索记录数等,需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题成为基数问题。
解决基数问题有很多种方案。
- 数据存储在Mysql表中,使用distinct count 计算不重复个数。
- 使用Redis 提供的hash、set、bitmaps 等数据结构来处理。
以上方案结构精确,但随着数据不断增加,导致空间越来越大,对于非常大的数据集是不切实际的。能否降低一定的精度来平衡存储空间?Redis 推出HyperLogLog。
- Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog的优点是:在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且很小。
- 在Redis 里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费的内存就越多的集合形成鲜明的对比。
- 但是,因为HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集{1,3,5,7,5,7,8},那么这个数据集的基数集为{1,3,5,7,8},基数为5,基数估计就是在误差可接受的范围内,快速计算基数。
五、Geospatial
Redis在3.2中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的二维坐标,在地图上就是经纬度。Redis基于该类型,提供了经纬度设置,查询、范围查询、距离查询、经纬度Hash等常见操作。
六、Redis的发布与订阅
1.什么是发布与订阅
- Redis 发布订阅(pub/sub)是一种消息通信模式:发布者发布消息,订阅者接受消息。
- Redis客户端可以订阅任意数量的频道。
2.Redis的发布与订阅
- 客户端可以订阅频道:
- 当这个频道发布消息后,消息就会发送给订阅的客户端:
七、Redis 事务
1.Redis 事务定义
Redis 事务是一个单独的隔离操作:事务中所有的命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
2.Multi、Exec、discard
Redis事务中有Multi、Exec、discard三个命令,从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,知道输入Exec后,Redis 会将之前的命令队列中的命令依次执行。而组队的过程中,可以通过discard 来放弃组队。
案例说明:
组队成功,提交成功。
组队阶段报错,提交失败。
组队成功,提交有成功有失败的情况。
3.事务的错误处理
3.1 组队中某个命令出现了报告错误,执行时整个的所有队列都会取消。
3.2 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
4.为什么要事务
想想一个场景:有很多人有你的账户,同时去参加双11抢购
5.事务冲突的问题
例子:
- 一个请求想给金额减8000;
- 一个请求想给金额减5000;
- 一个请求想给金额减1000;
最终我们可以发现,总共金额10000,如果请求全部都执行,那最后的金额变为-4000,很明显不合理。
6.悲观锁:
悲观锁,顾名思义,就是很悲观,每次去拿数据的时候 都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。
7.乐观锁:
乐观锁(Optimistic Lock) ,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适合于多读的应用场景,这样可以提高吞吐量。Redis 就是利用这种check-and-set 机制实现事务的。
watch Key[key ...]
在实行multi之前,先执行watch key1 [key2],可以监视一个或多个key,如果在事务执行之前这个 key被其他命令所改动,那么事务将被打断。
unwatch
取消WATCH 命令对所有key的监视。如果在执行Watch 命令之后,Exec命令或Discard 命令先被执行了的话,那么就不需要要Unwatch了。
8.Redis事务三特性
- 单独的隔离操作:事务种所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
- 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
八、Redis 持久化之RDB
Redis提供了2个不同形式的持久化方式:
RDB(Redis Database)
AOF(Append of file)
Redis 持久化之RDB
- 简介:在指定的时间间隔内,将内存中的数据集快照写入磁盘,也就是行话讲的SnapShot快照,它恢复时时间快照文件直接读到内存里。
- 备份是如何执行的:Redis 会Fork一个子进程来进行持久化,首先会将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就保证了极高的性能。如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化的数据可能丢失。
- Fork
- Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、PC等)都和原进程一样,但是是一个全新的进程,并作为原进程的子进程。
- 在Linux 程序中,fork() 会产生一个和父进程完全相同的子进程,但子进程在此后都会Exec 系统调用,出于效率考虑,Linux 中引入了写时拷贝技术。
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段内容要发生变化时,才会将父进程的内容拷贝一份给进程。
- RDB 持久化流程
- dumo.rdb 文件
在redis.conf 中,rdb 文件名默认为dump.rdb.
rdb 文件的保存路径,也可以修改。默认为redis 启动时命令行所在的目录下 ”dir ./“
- 如何触发RDB快照;保持策略。
配置文件中默认的快照配置
命令 save VS bgsave
- save:save 只管保存,其他不管,全部阻塞。
- bgsave:redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。
- 可以通过lastsave命令获取最后一次成功执行快照的时间。
flushall命令
执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义。
优势:
- 适合大规模的数据恢复。
- 对数据完整性和一致性要求不高更适合。
- 节省磁盘空间。
- 恢复速度快
劣势:
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
- 虽然Redis 在Fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis 意外Down掉的话,就会丢失最后一次快照后的所有修改。
如何停止
动态停止RDB: redis-cli config set save "" #save 后给空值,表示禁用保存策略。
小总结:
Redis 持久化之AOF
1.AOF(Append of File)
以日志的形式来记录每个写操作(增量保存),将Redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前往后执行一次已完成数据的恢复工作。
2.AOF持久化流程
- 客户端的请求写命令将会被append 追加到AOF 缓冲区中。
- AOF 缓冲区根据AOF持久化策略[always,sverysec,no] 将操作sync 到磁盘的AOF 文件中;
- AOF 文件大小超过重写策略或手写重写时,会对AOF文件rewrite 重写,压缩AOF 文件容量
- Redis 服务重启时,会重新load 加载aof 文件中的写操作达到数据恢复的目的。
3.AOF 默认不开启
可以在redis.conf 中配置文件名称默认为appendonly.aof 文件中开启,AOF 文件的保存路径与RDB 文件的路径一致。
4.AOF 和RDB 同时开启,redis 挺听谁的?
AOF 与RDB 同时开启,系统默认取AOF 的数据(数据不会存在丢失)
5.AOF的启动、修复、恢复
- AOF 的备份机制和性能虽然和RDB 不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝带redis工作目录下,启动系统即加载。
- 正常恢复
- 修改默认的appendonly no 改为yes
- 将有数据的aof 文件复制一份到对应的目录。(查看目录:config get dir)
- 恢复:重启redis然后重新加载。
- 异常恢复:
- 修改默认的appendonly no 改为yes
- 如遇到AOF文件损坏,通过redis-check-aof-fix apendonly.aof 进行恢复。
- 备份被写坏的AOF文件
- 恢复:重启Redis,然后重新加载。
6.AOF 同步频率设置
- appendfsync always:始终同步,每次redis 的写入都会立刻写入日志;性能较差但数据完整性
比较好。
- appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
- appendfsync no:redis 不主动进行同步,把同步时机交给操作系统。
7.rewrite压缩
- rewrite 压缩是什么?
AOF 采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF 文件的大小超过所设定的阈值时,Redis就会启动AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof。
- 重写原理,如何实现重写
AOF 文件持续增长而过大时,会fork 出一个新进程来将文件重写(也是先写临时文件最后再rename),redis 4.0版本后的重写,是指把rdb 的快照,以二进制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
- no-appendfsync-on-rewrite
- 如果no-appendfsync-on-rewrite=yes,不写入aof 文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机,会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
- 如果no-append-fsync-on-rewriye=no,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
- 触发机制,何时重写
redis 会记录上次重写时的AOF文件大小,默认配置是当AOF文件大小是上次rewrite 后大小的一倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间。但每次重写还是有一定的负担的,因此设定redis 要满足一定条件才会进行重写。
- aoto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时,开始重写(文件是原来重写后文件的两倍是触发)
- auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
- 系统载入时或者上次重写完毕时,redis 会记录此时AOF大小,设为base_size
- 如果redis的AOF 当前大小 >= base_size + base_size * 100%(默认)且当前大小 >= 64MB(默认) 的情况下,redis会对aof进行重写。
- 例如:当文件达到 70MB开始重写,降到50MB,下次什么时候开始重写?100MB!
重写流程:
1.bgrewriteaof 触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
2.主进程fork 出子进程执行重写操作,保证主进程不会阻塞。
3.子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf 重写缓冲区,保证原AOF 文件完整以及新AOF 文件生成期间的新的数据修改动作不会丢失。
4.子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。主进程把aof_rewrite_buf中的数据写入新的AOF文件。
5.使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
优势
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文件,通过操作AOF稳健,可以处理误操作。
劣势:
- 比起RDB 占用更多的磁盘空间
- 恢复备份速度较慢
- 每次读写都同步的话,有一定的性能压力
- 存在个别Bug,造成恢复不能。
小总结:
总结:
用哪个好
官方推荐两个都启用:
- 如果对数据不敏感,可以选单独用RDB
- 不建议单独用AOF,因为可能会出现Bug
- 如果只是做存内存缓存,可以都不用。
官网建议
- RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。
- AOF持久化方式记录每次对服务器的写操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。
- Redis还能对AOF文件进行后来重写,使得AOF文件的体积不至于过大。
- 只做缓存:如果你希望你的数据在服务器运行的时候存在,你也可以不适应任何持久化方式。
- 同时开启两种持久化方式:在这种情况下,当redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集完整。
- RDB 的数据不实时,同时使用两者时,服务器重启也只会找AOF文件。那要不要只使用AOF呢?
- 建议不要,因为RDB更适合用户备份数据库,快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
- 性能建议:
- 因为RDB 文件只用作后备用途,建议只在salve 上持久化RDB文件,而且只要15分组备份一次就够了,只保留save 900 1这跳原则。
- 如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒的数据,启动脚本较简单,只reload自己的AOF文件就可以了
- aof代价:一是带来了持续的IO,二是AOF rewrite 的最后,将rewrite 过程中产生的新数据写到新文件造成的阻塞几乎不可避免。
- 只要硬盘许可,应该尽量减少AOF rewrite 的频率,AOF重写的基础大小默认64M太小了,可以设到5G以上。默认超过原大小 100%大小时重写可以改到适当的数值。
九、Redis主从同步
主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,master以写为主,slave以读为主,主从复制结点间数据是全量的。
作用:
- 读写分离,性能扩展
- 容灾快速恢复
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上所有的数据都复制一份,具体步骤如下:
- slave服务器连接到Master服务器,便开始进行数据同步,发送psync命令(redis2.8之前是sync 命令)
- master服务器收到psync命令之后,开始执行bgsave命令生成RDB快照文件并使用缓冲区记录此后执行的所有写命令;如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是每个连接都执行一次,然后再把这一份持久化的数据发送给多个并发连接的slave,如果RDB 复制时间超过60秒,那么slave服务器就会认为复制失败,可以适当调节大这个参数。
- master服务器bgsave执行完之后,就会向所有slave服务器发送快照文件,并在发送期间继续向缓冲区内记录被执行的写命令。client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败。
- slave 服务器收到RDB快照文件后,会将收到的数据写入磁盘,额案后清空所有旧数据,再从本地磁盘载入收到的快照到内存中,同时基于旧的数据版本对外提供服务。
- master服务器发送完RDB快照文件之后,便开始向slave服务器发送缓冲区中的写命令。
- salve服务器完成对快照的载入,开始接受命令请求,并执行来自主服务器缓冲区的写命令。
- 如果salve node 开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF。
哨兵模式
反客为主:当一个master 宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。用slaveof no one 指令将从机变为主机。而哨兵模式是反客为主的的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从机转换成主机。
当主机挂掉,从机选举产生新的主机
- 哪个从机会被选举为主机呢?根据优先级别:slave-priority.
- 原主机重启后变成从机。
复制延时:
由于所有的写操作都是现在master上操作,然后同步更新到slave上,所以从Master同步到slave 机器有一定的延迟,当系统很繁忙时,延迟问题更加严重,Slave 机器数量的增加也会使得=这个问题更加严重。
故障恢复:
优先级:在redis.conf 中默认slave-priority 100,值越小优先级越高。
偏移量: 指获得原主机数据最全的概率。
runid:每个redis 实例启动后都会随机产生一个40位的runid。
十、Redis应用问题解决
1.缓存穿透
- 问题描述
key 对应的数据在数据源并不存在,每次针对此key的请求从缓存中获取不到,请求都会压到数据源(数据库),从而可能压垮数据源。比如:用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞攻击可能压垮数据库。
缓存击穿发生的条件:
- 应用服务器压力变大
- redis 命中率降低
- 一直查询数据库,使得数据库压力太大而压垮
其实redis 在这个过程中一直平稳运行,崩溃的是数据库。
缓存穿透发生的原因:黑客或者其他非正常用户频繁进行很多非正常url的访问,使得redis查询不到数据库。
解决方案:
- 对空值缓存:如果一个查询返回的数据为空(不管数据是否不存在),我们仍然需要把这个空结果进行缓存,设置空结果过期时间会很短,最长不超过5min.
- 设置可访问的名单(白名单):使用bitmaps 类型定义一个可以访问的名单,名单id作为bitmaps 的偏移量,每次访问和bitmaps 里面的id 进行比较,如果访问id不在bitmaps 里面,进行拦截,不允许访问。
- 采用布隆过滤器:布隆过滤器(bloom filter)是1970年有布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询效率都远超一般的算法。缺点是有一定的误识率和删除困难。
- 进行实时监控:当发现Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
2.缓存击穿
- 问题描述:key对应的数据存在,但在redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。
- 缓存击穿的现象
- 数据库访问的压力瞬间增加,数据库崩溃
- redis 里面没有出现大量key过期
- redis 正常运行
- 缓存击穿发生的原因:redis某个key过期了,大量访问使用这个key(热门key)
- 解决方案:key可能会在某些时间点被超高并发地访问,是一种非常热点的数据。
- 预先设置热门数据:在redis 高峰访问之前,把一些热门数据提前存入redis里面,加大这些热门数据key的时长。
- 实时调整:现场监控哪些数据热门,实时调整key的过期时长。
- 使用锁:
- 就是在缓存失效的时候(判断拿出来的值为空),不是立刻去load db.
- 先使用缓存工具的某些带成功操作返回值的操作(比如redis的SETNX) 去set一个mutex key.
- 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key。
- 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
3.缓存雪崩
- 问题描述:key 对应的数据还在,但在redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设缓存,这个时候大并发的请求可能会瞬间将数据库压垮。缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key正常访问。
缓存失效瞬间:
- 解决方案:
- 构建多级缓存架构:nginx 缓存+ redis 缓存+本地缓存。
- 使用锁或者队列:用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,该方法不适合高并发情况。
- 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
- 将缓存失效的时间分散开:比如可以在原有的失效时间基础上增加一个随机值,比如1-5min随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体的失效事件。
十一、分布式锁
setnx:通过该命令尝试获得锁,没有获得锁的线程会不断等待尝试。
set key ex 3000 nx: 设置过期时间,自动释放锁,解决当某一个业务 异常而导致无法释放的问题。但是当业务运行超过过期时间,开辟监控线程增加该锁的时间,知道业务运行完毕。‘
uuid:设置uuid,释放锁时,判断是否是自己的锁,方式误删除锁,造成没锁的情况。
十二、RedLock
Redlock 是一种算法,Redlock 也就是Redis distributed lock,可是实现多结点redis 的分布式锁。redislock 官方推荐,redission 完成了对redlock 算法封装。
此种方式具有一下特性:
- 互斥访问:即永远只有一个client能拿到锁。
- 避免死锁:最终client 都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。
- 容错性:只要大部分redis结点存活(一半以上),就可以正常提供服务。
RedLock 原理:
- 获取当前unix时间,以毫秒为单位。
-
依次尝试从 N 个实例,使用相同的 key 和随机值获取锁。在步骤 2,当向 Redis 设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以避免服务器端 Redis 已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个 Redis 实例。
-
客户端使用当前时间减去开始获取锁时间(步骤 1 记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是 3 个节点)的 Redis 节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
-
如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
-
如果因为某些原因,获取锁失败(没有在至少 N/2+1 个 Redis 实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功)。