个人技术分享

Redis

Redis概述

        redis是一个开源的key-value存储系统。Redis不仅仅是一个简单的缓存系统,还可以用作消息队列、计数器等功能。

        Redis 是单线程+多路 IO 复用技术。多路复用是指使用一个线程来检查多个文件描述符(socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞知道超时。得到就绪状态后进行真正的操作,可以在同一个线程里执行。

  1. 内存存储:Redis主要将数据存储在内存中,因此读写速度非常快。同时,Redis也支持将数据持久化到磁盘中,以实现数据的持久化存储。

  2. 数据结构多样:Redis支持多种数据结构,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。这些数据结构具有丰富的操作命令,可以方便地进行数据操作和计算。

  3. 高并发性能:Redis采用了单线程的事件驱动模型,通过异步IO操作来提高并发性能。此外,Redis还使用了基于内存的数据结构和一些高效的算法,使其在读写操作中具有极高的性能。

  4. 发布/订阅机制:Redis提供了发布/订阅(Pub/Sub)机制,可以实现消息的发布和订阅,并支持多个客户端之间的实时消息通信。

  5. 事务支持:Redis支持事务操作,通过MULTI、EXEC、DISCARD和WATCH等指令可以实现一组操作的原子性和隔离性。

  6. 高可用性:Redis支持主从复制和哨兵机制,可以实现数据的备份和故障转移,提高系统的可用性和容错性。

  7. 持久化支持:Redis提供了两种持久化方式,分别是RDB(Redis Database)和AOF(Append-Only File)。RDB是通过将内存中的数据定期快照存储到磁盘中,而AOF则是通过追加写日志的方式来记录每个写操作,以实现数据的持久化。

  8. 扩展性:Redis支持集群模式,可以通过横向扩展的方式增加节点,以满足大规模数据存储和高并发访问的需求。

应用场景

1、配合关系型数据库做高速缓存。

        高频次、热门访问的数据,降低数据库IO;分布式架构做session共享。

2、多样的数据结构存储持久化数据。

        最新N个数据  --  使用List实现按自然时间排序的数据。

        排行榜            -- 利用zset有序集合。

        时效性数据    -- 设置过期时间

        计数器         -- 原子性,自增方法INCR        

        去重复数据 -- set集合

        发布订阅消息 --  pub/sub模式

常用五大数据类型

redis键(key)

keys *    --查看当前库所有key

exists key  --判断某个key是否存在

type key   --查看key是什么类型

del key   --删除指定的key数据

unlink key  --根据value选择非阻塞删除(仅将 keys 从 keyspace 元数据中删除,真正的删除会在后续异步操作)

expire key 10  --未key设置10秒钟的过期时间

ttl key  --查看还有多少秒过期  -1表示永不过期   -2表示已过期

Redis字符串(String)

String类型是redis最基本的数据类型,一个Redis字符串value最多是512M.

String类型是二进制安全的,所以Redis的String可以包含任何数据,比如jpg或者序列化的对象。

内部结构采用预分配冗余空间的方式来减少内存的频繁分配,当字符串长度小于1M时,每次扩容都加倍,当大于1M时,每次扩1M的空间。

Redis列表(List)

Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头 部(左边)或者尾部(右边),它的底层实际上相当于一个双向链表,对两端的操作性能很高,通过索引下标的操作中间节点的性能会较差。

        List 的数据结构为快速链表 quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量较多的时候才会改为quikList。因为普通的链表需要的附加指针空间太大,会比较浪费空间。

        Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指 针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

Redis集合(Set)   

Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

Redis Set 是String类型的无序集合,它底层其实是一个value为null的hash表,所以添加,删除查找的复杂度都是O(1)

时间复杂度是随着数据的增加,执行时间的长短,如果是O(1),数据增加查找数据的时间不变。

set的数据结构是dict字典,字典使用哈希表实现的。

Java 中 HashSet 的内部实现使用的是 HashMap,只不过所有的 value 都指向同一个对象。 Redis 的 set 结构也是一样,它的内部也使用 hash 结构,所有的 value 都指向同一个内部值。

Redis哈希(Hash)

Redis hash是一个String类型的fiel和value的映射表,特别适合用于存储对象。

Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用 ziplist,否则使用 hashtable。

Redis 有序集合(Zset)

Redis的有序集合zset和普通集合set类似,是一个没有重复元素的字符串集合。

不同之处在于zset每一个成员都关联了一个score,这个score被用来按照从最低分到最高分的方式排序集合中的成员,集合成员是唯一的,但是score可以重复。

SortedSet(zset)是 Redis 提供的一个非常特别的数据结构,一方面它等价于 Java 的数据结Map,可以给每一个元素 value 赋予一个权重 score,另 一方面它又类似于 TreeSet,内部的元素会按照权重 score 进行排序,可以得到每个元 素的名次,还可以通过 score 的范围来获取元素的列表。 zset 底层使用了两个数据结构:

        (1)hash,hash 的作用就是关联元素 value 和权重 score,保障元素 value 的唯 一性,可以通过元素 value 找到相应的 score 值。

        (2)跳跃表,跳跃表的目的在于给元素 value 排序,根据 score 的范围获取元素列表。

Redis的发布和订阅

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。 Redis 客户端可以订阅任意数量的频道。

客户端可以订阅频道:

当这个频道发布消息后,消息就会传递给订阅的客户端:

Redis持久化

RDB

        在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是 Snapshot 快照,它恢复时是将快照文件直接读到内存里。

        Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。        

fork

⚫ Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、 程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

⚫ 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了“写时复制技术”。

⚫ 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

RDB 持久化流程

优势

⚫ 适合大规模的数据恢复

⚫ 对数据完整性和一致性要求不高更适合使用

⚫ 节省磁盘空间

⚫ 恢复速度快

劣势

⚫ Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑

⚫ 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。

⚫ 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话, 就会丢失最后          一 次快照后的所有修改。

AOF

        以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF持久化流程

(1)客户端的请求写命令会被 append 追加到 AOF 缓冲区内;

(2)AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的 AOF 文件中;

(3)AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量;

(4)Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的;

Rewrite

AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩, 只保留可以恢复数据的最小指令集。AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写。

(1)bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行。

(2)主进程 fork 出子进程执行重写操作,保证主进程不会阻塞。

(3)子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失。

(4)

        1).子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息。

        2).主 进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件。

(5)使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写。

优势和劣势

◼ 备份机制更稳健,丢失数据概率更低。

◼ 可读的日志文本,通过操作 AOF 稳健,可以处理误操作。

◼ 比起 RDB 占用更多的磁盘空间。

◼ 恢复备份速度要慢。

◼ 每次读写都同步的话,有一定的性能压力。

Redis_主从复制

        主机数据更新后根据配置和策略, 自动同步到备机的 master/slaver 机制,Master 以 写为主,Slave 以读为主。

⚫ 读写分离,性能扩展

⚫ 容灾快速恢复

        如果为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC 命令给master请求复制数据。 master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后 再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。 当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多 个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送 给多个并发连接的slave。

哨兵模式

        能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

1、从下线的主服务的所有从服务中挑选出一个,将其转换成主服务

        a选择优先级靠前的

        b选择偏移量最大的

        c选择runid最小的

2、挑选出新的主服务后,哨兵向原主服务的从服务发送slaveof新主服务的命令,复制新master。

3、当已下线的服务重新上线时,会成为新主的从

Redis集群

Redis 集群实现了对 Redis 的水平扩容,即启动 N 个 redis 节点,将整个数据库分布存储在这 N 个节点中,每个节点存储总数据的 1/N。

Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。

即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。 无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。

实现扩容

分摊压力

无中心配置相对简单

Redis应用问题解决

缓存穿透

        key 对应的数据在数据源并不存在,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。

解决方案

        一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

(1) 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。

(2) 设置可访问的名单(白名单): 使用 bitmaps 类型定义一个可以访问的名单,名单 id 作为 bitmaps 的偏移量, 每次访问和 bitmap 里面的 id 进行比较,如果访问 id 不在 bitmaps 里面,进 行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。 它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

        当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

        布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得 比较均匀。 向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就 完成了 add 操作。 向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个 位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这 个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位 数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。 这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。

(4) 进行实时监控:当发现 Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。

缓存击穿

        key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。

解决方案

        key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

(1)预先设置热门数据:在 redis 高峰访问之前,把一些热门数据提前存入到 redis 里面,加大这些热门数据 key 的时长。

(2)实时调整:现场监控哪些数据热门,实时调整 key 的过期时长

(3)使用锁

        就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db。而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX) 去 set 一个 mutex key。当操作返回成功时,再进行 load db 的操作,并回设缓存,最后删除 mutex key。当操作返回失败,证明有线程在 load db,当前线程睡眠一段时间再重试整个 get 缓存的方法。

缓存雪崩

        key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。 缓存雪崩与缓存击穿的区别在于这里针对很多 key 缓存,前者则是某一个 key。

解决方案

(1) 构建多级缓存架构:nginx 缓存 + redis 缓存 +其他缓存(ehcache 等。

(2) 使用锁或队列: 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行 读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。

(3) 设置过期标志更新缓存: 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。

(4) 将缓存失效时间分散开: 比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存与数据库双写不一致

        1、双写不一致情况

        

        2、读写并发不一致

        

解决方案

 1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

3、如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。

4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

        针对读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存了,可以直接操作数据库。如果数据库抗不住压力,还可以把缓存作为数据读写的主存 储,异步将数据同步到数据库,数据库只是作为数据的备份。 放入缓存的数据应该是对实时性、一致性要求不是很高的数据。切记不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统复杂性!

IO多线程

        IO 多线程其实指客户端交互部分的网络 IO 交互处理模块多线程,而非执行命令多线程。Redis6 执行命令依然是单线程。