redis篇


一个支持多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。

1、五种类型

  • String类型:它是一个二进制安全的字符串,意味着它不仅能够存储字符串、还能存储图片、视频等多种类型, 最大长度支持512M。

    操作命令:

    • GET/MGET

    • SET/SETEX/MSET/MSETNX

    • INCR/DECR

    • GETSET

    • DEL

  • 哈希类型:该类型是由field和关联的value组成的map。其中,field和value都是字符串类型的。

    操作命令:

    • HGET/HMGET/HGETALL

    • HSET/HMSET/HSETNX

    • HEXISTS/HLEN

    • HKEYS/HDEL

    • HVALS

  • 列表类型:该类型是一个插入顺序排序的字符串元素集合, 基于双链表实现。

    操作命令:

    • LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET

    • LINDEX/LRANGE

    • LLEN/LTRIM

  • 集合类型:Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。Set类型的底层是通过哈希表实现的。

    操作命令:

    • SADD/SPOP/SMOVE/SCARD

    • SINTER/SDIFF/SDIFFSTORE/SUNION

  • 顺序集合类型:ZSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。与Set类型一样,其底层也是通过哈希表实现的。

    操作命令:

    • ZADD/ZPOP/ZMOVE/ZCARD/ZCOUNT

    • ZINTER/ZDIFF/ZDIFFSTORE/ZUNION

2、数据结构

img

  1. 压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,Redis就会使用压缩列表来做列表键的底层实现。

  2. 整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

3、单线程or多线程

Redis单线程指的是执行Redis命令的核心模块是单线程的,而不是整个Redis实例就一个线程,Redis其他模块还有各自模块的线程的。

img

Redis的瓶颈并不在CPU,而在内存和网络。如果要使用CPU多核,可以搭建多个 Redis 实例来解决。

Redis 6版本中的多线程IO指的是在网络IO处理方面上了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。

4、事务

Redis单条命令是保证原子性的,但是Redis的事务不保证原子性。

  • 本质:Redis事务就是一组命令的集合。一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行。

  • 特点:Redis事务具有一次性、顺序性、排他性。不保证原子性,没有隔离级别的概念。

  • 执行过程:

    1. 开启事务

    2. 命令入队

    3. 执行事务

 // 开启事务
 redis> MULTI
 OK
 ?
 // 命令入队
 redis> SET msg "hello moto"
 OK
 ?
 // 执行事务
 redis> exec
 1) OK

4.1、放弃事务

使用discard命令

 // 开启事务
 redis> MULTI
 OK
 ?
 // 命令入队
 redis> SET msg "hello moto"
 OK
 ?
 // 执行事务
 redis> exec
 1) OK

5、实现乐观锁

  • 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。(可以使用版本号机制和CAS算法实现)

5.1、WATCH监视器

利用watch命令对对象(key)进行监听,获取对象的version版本,在对象发生更新的时候会比较这个version版本,发现不一样就表示这个对象在此期间被别人更新了,自此就更新也就失败了。

5.2、UNWATCH解除监视器

利用unwatch命令可以解除被watch监视器监视对象的监听效果。

6、实现分布式锁

JDK中所提供的的lock锁和synchronized关键字只能解决单体服务的并发问题。而要实现分布式环境下的锁则可以通过Redis来实现分布式锁。Redis实现分布式锁主要是运用了Redis的SETNX(SET if Not eXists) 指令和lua脚本来实现。

  • setnx命令:是SET IF NOT EXISTS(如果不存在,则 SET)的简写。他的语法是SETNX key value

  • 需要lua脚本的原因:我们要给锁加一个过期时间一般都需要拆分成两个步骤,第一个将key存入Redis,第二部给key设置过期时间,这个时候就会导致线程不安全性。因为 Lua 脚本可以保证多个指令的原子性执行(单线程基于I\O多了复用和对列执行命令)。

6.1、Redisson实现原理

  • 加锁机制:就是上面提到的SETNX(SET if Not eXists)指令和lua脚本来实现。

  • watch dog自动延期机制:就是会启动一个后台线程,定时比如说10秒检测当前线程是否还持有了锁,是的话就延期。

  • 可重入性:Redis存储锁的数据类型是 Hash类型并且Hash数据类型的key值包含了当前线程信息。

  • 互斥性:通过Redis数据结构来保证分布式锁的唯一性。

  • 避免死锁:通过Redis的超时时间保证。

img

7、SpringBoot集成Redis

在SpringBoot2.x之后,原来使用的jedis被替换为lettuce。

  • jedis:采用直连,多个线程操作是不安全的,想要避免线程不安全,可以使用jedis pool连接池。更像BIO模式。

  • lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的问题。更像NIO模式。

8、配置文件

redis.conf文件是Redis的配置文件。

9、持久化

Redis是内存数据库,存放数据的位置是在内存当中,如果不能将内存中的相关数据保存到磁盘去,那么一旦出现一些异常导致内存被清空就会出现问题。所以Redis提供了持久化功能。

redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。

9.1、RDB方式

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

img

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

RDB保存的文件是dump.rdb(可以在配置文件中更改这个快照文件的名称dbfilename dump.rdb)。

9.1.1、触发条件

RDB持久化的触发分为手动触发和自动触发两种。

9.1.1.1、手动触发
  • save命令:会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求。

  • bgsave命令:会创建一个子进程,由子进程来负责创建RDB文件,父进程(即Redis主进程)则继续处理请求。

  • flushall命令

  • shutdown命令:Redis退出命令。

bgsave命令执行过程中,只有fork子进程时会阻塞服务器,而对于save命令,整个过程都会阻塞服务器,因此save已基本被废弃,线上环境要杜绝save的使用;

9.1.1.2、自动触发

自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave

image-20220218103618646

其中save 900 1的含义是:当时间到900秒时,如果redis数据发生了至少1次变化,则执行bgsave;save 300 10和save 60 10000同理。当三个save条件满足任意一个时,都会引起bgsave的调用。

在主从复制场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件发送给从节点

9.2、AOF方式

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

AOF保存的文件是appendonly.aof。(可以在配置文件中更改这个快照文件的名称appendfilename "appendonly.aof")。

默认是不开启的,需要在配置文件redis.conf中配置appendonly yes

如果aof文件大于Redis.conf配置文件auto-aof-rewrite-min-size 64mb的值,就会fork一个新的进程来讲我们的文件进行重写。

由于需要记录Redis的每条写命令,因此AOF不需要触发。

AOF的执行流程包括:

  • 命令追加(append):将Redis的写命令追加到缓冲区aof_buf;

  • 文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘;

  • 文件重写(rewrite):定期重写AOF文件,达到压缩的目的。

9.2.1、命令追加

Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。

9.2.2、文件写入和文件同步

为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。

AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:

  • always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。

  • no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。

  • everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。

9.2.3、文件重写

定期重写AOF文件,减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作!

文件重写之所以能够压缩AOF文件,原因在于:

  • 过期的数据不再写入文件

  • 无效的命令不再写入文件:如有些数据被重复设值(set mykey v1, set mykey v2)、有些数据被删除了(sadd myset v1, del myset)等等

  • 多条命令可以合并为一个:如sadd myset v1, sadd myset v2, sadd myset v3可以合并为sadd myset v1 v2 v3。不过为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。

9.2.3.1、文件重写的触发

文件重写的触发,分为手动触发和自动触发。

  • 手动触发:直接调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程进行具体的工作,且都只有在fork时阻塞。

  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。

    • auto-aof-rewrite-min-size:执行AOF重写时,文件的最小体积,默认值为64MB。

    • auto-aof-rewrite-percentage:执行AOF重写时,当前AOF大小(即aof_current_size)和上一次重写时AOF大小(aof_base_size)的比值。

10、订阅发布

  • 订阅端:SUBSCRIBE 信道名

  • 发送端:PUBLISH 信道名 “消息”

11、集群

11.1、主从复制

img

将一台Redis服务器的数据,复制到其他Redis服务器。前者为主节点(master/leader),后者为从节点(slave/follower)。数据的复制是单向的,只能由主到从。主节点已写为主,从节点以读为主。

默认情况下,每台Redis服务器都是主节点,一个主节点可以有多个从节点或没有从节点,但是一个从节点只有一个主节点。

主从复制的主要作用:

  1. 数据冗余:实现数据的热备份,是持久化之外的一种数据冗余方式。

  2. 故障恢复:主节点出现问题时,由从节点提供服务,实现快速的故障恢复。实际上是一种服务冗余。

  3. 负载均衡:在主从复制的基础上,配合读写分离,主节点提供写服务,从节点提供读服务,分担服务器负载,提高Redis服务器的并发量。

  4. 高可用基石:主从复制还是哨兵模式和集群能够实现的基础,因此可以说主从复制石Redis高可用的基础。

11.1.1、特点

主节点断开,没有写操作(此时没有主节点);主节点重新连接,主节点依旧可以写,且让从节点读取的到主节点写的数据(又有主节点了)。

集群中新加入的从节点,会同步主节点上的数据到这个从节点上。原理就是:从节点启动连接到主节点后会发送一个sync同步命令,主节点收到请求命令后,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,主节点将传送整个数据文件到从节点,并完成一次完全同步。

  • 全量复制:从节点在接收到数据库文件命令后,将其存盘并加载到内存中。

  • 增量复制:主节点继续将新的所有收集到的修改命令依次传递给从节点,完成同步。

但是只要是重新连接到主节点,一次全量复制将被自动执行。

11.2、主从复制(变种)

image-20220218114916485

这种方式模式是主机 --- 从机or主机 --- 从机,属于主从复制的一种变种架构。

11.2.1、特点

此时既当从节点又当主节点的Redis服务器其实在角色定义上仍是从节点。

当真实的主节点离开之后,从节点可手动执行SLAVEOF no one命令来变为一个主节点。如果这时候原本离开的真实主节点重新上线了,那么它就不在这个集群体系里面了。

11.3、哨兵模式

主从复制模式中当主节点宕机了,整个集群就没有可写的节点了。而哨兵模式可以在主节点宕机之后,能够将从从节点中推选一个出来变成一个主节点。

img

Redis提供了哨兵命令,哨兵是一个独立的进程,作为进程,他会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行多个Redis实例

注意:如果原本宕机的主服务器重新上线了,那么就会成为被选举出来的新主机下做为从机工作。

11.3.1、哨兵的作用

  • 监控(Monitoring):Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。

  • 提醒(Notification):当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

  • 自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会进行选举,将其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器,通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。

11.3.2、多哨兵模式

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

img

故障切换(failover)的过程:假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行故障切换过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

12、缓存问题

假如我们使用Redis作为MySQL数据的查询缓存。那么现在有这样一个案例:前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果;数据库也没取到,那直接返回空结果。

img

 

12.1、缓存一致性问题

读数据的时候首先去Redis里读,没有读到再去MySQL里读,读回来之后更新到Redis里作为下一次的缓存。写数据的时候回产生数据不一致的问题,无论是先写到Redis里再写MySQL,还是先写MySQL再写Redis,这两步写操作不能保证原子性,所以会出现Redis和MySQL里的数据不一致。无论采取何种方式都不能保证强一致性,如果对Redis里的数据设置了过期时间能够保证最终一致性,对架构做的优化只能降低不一致性发生的概率,不能从根本上避免不一致性。

img

12.2、穿透、击穿、雪崩

img

12.2.1、缓存穿透

preview

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,加大了数据库的压力。

解决方案:

  • 缓存中设置对应的空值key:对不存在的数据缓存到redis中,设置key,value值为null(不管是数据未null还是系统bug问题),并设置一个短期过期时间段,避免过期时间过长影响正常用户使用。

  • 将请求IP地址添加到黑名单中,禁止他访问。

  • 对参数进行校验,不合法参数进行拦截。

  • 添加布隆过滤器:将所有可能存在的数据以哈希的形式放到一个足够大的bitmap(位图)中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

12.2.2、缓存击穿

preview

缓存击穿是指缓存中某一个热点key在不停地扛着高并发,当这个热点key在失效的一瞬间,持续的高并发访问就击破缓存直接访问数据库,导致数据库瞬间承受了过大的压力。好比在一个屏障上凿开了一个洞。

解决方案:

  • 设置热点数据永不过期。

  • 加互斥锁(分布式锁):上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它 其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后将数据放到redis缓存起来。后面的线程进来发现已经有缓存了,就直接走缓存。

12.2.3、缓存雪崩

preview

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  • 随机设置key失效时间,避免大量key集体失效。setRedis(Key, value, time + Math.random() * 10000);

  • 若是集群部署,可将热点数据均匀分布在不同的Redis库中也能够避免key全部失效问题。

  • 数据预热:在正式部署之前,把可能的数据预先访问一遍,这样可能大量访问的数据就会加载到缓存中,设置不同的过期时间,让缓存失效的时间点尽量均匀。

  • 不设置过期时间。

  • 跑定时任务,在缓存失效前刷进新的缓存。

13、内存空间问题

13.1、设置最大内存

通过Redis配置文件redis.conf中的maxmemory值(单位:byte)来调整Redis的最大内存空间。一般推荐Redis设置内存为最大物理内存的四分之三。

13.2、淘汰策略

当Redis内存满了之后,可以通过三种方式来解决:

  • 增大内存:不推荐。不能解决根本问题。

  • 使用集群:集群需要增加额外的资源开销。

  • 设置淘汰策略:

    淘汰策略方式描述
    volatile-lru 仅对设置了过期时间的键采取LRU淘汰策略
    volatile-lfu 仅对设置了过期时间的键采取LFU淘汰策略
    volatile-ttl 仅淘汰设置了过期时间的键,淘汰生存时间TTL(Time To Live)更小的键
    volatile-random 随机淘汰设置了过期时间的键
    allkeys-lru 对所有的键都采取LRU淘汰
    allkeys-lfu 对所有的键都采取LFU淘汰
    allkeys-random 对所有的键随机淘汰
    noeviction 不淘汰,若超过最大内存,返回错误信息