1.Redis应用场景
解决CPU及内存压力
解决IO压力
案例1:配合关系型数据库做高速缓存:
高频次,热门访问的数据,降低数据库IO
分布式架构,做session共享
案例2:多样的数据结构存储持久化数据
2.几种特殊的数据结构
2.1 跳跃表
有序链表和跳跃表查找效率对比:
2.2 Bitmaps
现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图
合理地使用操作位能够有效地提高内存使用率和开发效率。Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
(1)Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
(2)Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
案例1:(setbit<key><offset><value>设置Bitmaps中某个偏移量的值(0或1))每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。设置键的第offset个位的值(从0算起),假设现在有20个用户,userid=1,6,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图:
备注:很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常做法是每次做setbit操作时将用户id减去这个指定数字。假如第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
案例2:getbit<key><offset>获取Bitmaps中某个偏移量的值
获取id=8的用户是否在2020-11-06这天访问过,返回0说明没有访问过。
案例3:bitcount<key>[start end] 统计字符串从start字节到end字节比特值为1的数量
计算2022-11-06这天的独立访问用户数量,
start和end代表起始和结束字节数,下面操作计算用户id在第一个字节到第三个字节之间的独立访问用户数,对应的用户id是11,15,19.
案例4:bitop and(or/not/xor) <destkey> [key…]bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
案例5:Bitmaps和set对比
2.3 HyperLogLog
涉及统计相关的需求,比如统计网站PageView页面访问量,可以使用Redis的incr等实现。但如果UV独立访客、独立IP数、搜索记录数等需要去重和计数的问题如何解决。这种求集合中不重复元素个数的问题称为基数问题。
HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且很小的。
案例1:pfadd <key>< element>[element ...] 添加指定元素到 HyperLogLog 中
案例2:pfcount<key> [key ...] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
案例3:pfmerge<destkey><sourcekey>[sourcekey ...] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
3.发布与订阅
Redis是一种发布订阅消息通信模式:发送者发送消息,订阅者接收消息。
4.事务、锁机制
4.1事务定义
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求打断。Redis事务的主要作用就是串联多个命令防止别的命令插队。
4.2 Muli、Exec、Discard
4.3 事物的错误处理
如果是组队阶段出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
4.4 为什么需要事务?
案例:很多人有你的账号,同时去参加双十一的抢购。
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到拿到锁。传统关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先上锁。
乐观锁:就是很乐观,每去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
Watch在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis事务的三特性:
单独的隔离操作:事务中的所有命令都会被序列化、按顺序执行。事务在执行的过程中,不会被其他客户端发送来的命令请求打端。
没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 。
5.秒杀
5.1 超卖问题
解决方法1:乐观锁(出现库存遗留问题和连接超时问题)
5.2 超卖问题
通过连接池
5.3 库存遗留问题
6.持久化(RDB、AOF)
6.1 RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。 bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。 可以通过lastsave 命令获取最后一次成功执行快照的时间
格式:save 秒钟 写操作次数 RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件, 默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。 禁用 不设置save指令,或者给save传入空字符串
6.2 AOF
以日志的形式来记录每个写操作(增量保存),将Redis执行过程的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
持久化流程:
客户端的请求写命令会被append追加到AOF缓冲区
AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
默认不开启,同时开启默认AOF,
Rewrite压缩:
重写流程:
优势:
劣势:
官方推荐两个都启用。
如果对数据不敏感,可以选单独用RDB。
不建议单独用 AOF,因为可能会出现Bug。
如果只是做纯内存缓存,可以都不用
因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。
代价,一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。
只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。
默认超过原大小
100%
大小时重写可以改到适当的数值。
7.主从复制
定义:主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,master以写为主,slave以读为主。
作用:读写分离,性能扩展;容灾快速恢复。
7.1搭建
复制redis6379,6380,6381多个redi是配置文件,模拟三台redis服务器
配置一台服务器为主另外两台为从。
一主二仆:
薪火相传:上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力,去中心化降低风险。slaveof <ip><port>
中途转向变更:会清除之前的数据,重新建立拷贝最新的。风险是一旦某个slave宕机,后面的slave都没办法备份。主机挂了,从机还是从机,无法写数据。
反客为主:当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。用slaveof no one将从机变为主机。
7.2 复制原理
slave启动成功连接到master后会发送一个sync命令。
Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次同步。
全量复制:而slave服务在接受到数据库文件数据之后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步。
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。
7.3 哨兵模式
反客为主的自动版,能够将后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
sentinel monitor mymaster 127.0.0.1 6379 1
mymaster为监控对象起的服务器名称,1为至少有多个哨兵同意迁移的数量。
选举从机的优先级别是slave-priority,原主机重启后会变为从机。
8.集群问题
容量不够,redis如何进行扩容?
并发写操作,redis如何分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。 之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。
定义:Redis集群实现了对Redis的水平扩容,即启动N个节点,将数据库分布存储在这N个节点中,每个节点存储总数居的1/N。Redis集群通过分区来提供一定程度的可用性:即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
构建六个实例:8.1 redis cluster如何分配这六个节点。
一个集群至少要有三个主节点。 选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。 分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
8.2 slots
一个Redis集群包含16384个插槽,数据库中的每个键都属于这16384个插槽的其中一个。集群使用公式CRC16(key)%16384来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC16校验和。
集群中的每个节点负责处理一部分插槽。举个例子,如果一个集群可以有主节点,其中:
节点A负责处理0号到5460号插槽。
节点B负责处理5461号至10922号插槽。
节点C负责处理10923号至16383号插槽。
注意:不在一个slot下的键值,是不能使用mget,mset等多建操作。
8.3 故障恢复
主节点下线,从节点能否自动升为主节点,注意15秒超时。
主节点恢复后,主节点变成从机。
如果某一段插槽的主从节点都宕机掉,而cluster-require-full-coverage 为yes,那么整个集群都挂掉。
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。
8.4 Redis集群的优点和不足
优点:实现扩容、分摊压力、无中心配置相对简单。
不足:多建操作不被支持,多建的Redis事务不被支持,lua脚本不被支持。
9.缓存穿透、击穿、雪崩和分布式锁
9.1 缓存穿透
key对应的数据在数据源并不存在,每次针对key的请求从缓存获取不到,请求都会被压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案:
对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
设置可访问的白名单:使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
采用布隆过滤器:实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数),布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间和查询效率都远超过一般的算法,缺点是有一定的误识别率和删除困难。将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问数据,和运维人员配合,可以设置黑名单限制服务。
9.2 缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决方案:
预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。
实时调整:现场监控哪些数据热门,实时调整key的过期时长。
-
使用锁:
9.3 缓存雪崩
缓存击穿和缓存雪崩的区别在于,缓存雪崩这里针对很多key缓存过期,而缓存击穿则是某一个key过期。
解决方案:
构建多级缓存架构:nginx缓存+redis缓存+其他缓存
使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
设置过期标值更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
将缓存失效时间分散开来:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
9.4 分布式锁
分布式集群后,分布式系统多线程、多进程并且分布在不同机器上,单机的并发控制锁策略就会失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。