Redis学习笔记
Redis学习笔记
Redis简介
Redis是一种键值型的NoSql数据库,是Remote Dictionary Server 远程词典服务器,是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
键值型,是指Redis中存储的数据都是以key、value对的形式存储,而value的形式多种多样
NoSql,是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库

扩展性:
关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦,但是当数据量过大的时候不得不进行水平扩展,此时对业务逻辑会带来很大的影响
Redis常见命令
Key的结构
Redis没有类似MySQL中的Table的概念
我们可以通过给key添加前缀加以区分,key的每一段之间使用 :分隔
通用命令
KEYS:使用通配符查看符合模板的所有key(生产环境应让禁止使用)
DEL:删除一个指定的key
EXISTS:判断key是否存在
TTL:查看一个KEY的剩余有效期
EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
String类型常用命令
value可以是普通字符串、整数、浮点数等(Redis 可能会使用整数编码(int 编码)来存储小的整数值)
SET、MSET
GET、MGET
INCR、INCRBY、INCRBYFLOAT:自增操作(非数值会报错)
SETNX:添加一个String类型的键值对,前提是这个key不存在
SETEX:添加一个String类型的键值对,并且指定有效期
Hash类型常用命令
Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD

HSET、HMSET
HGET 、HMGET、HGETALL
HKEYS:获取一个hash类型的key中的所有的field
HINCRBY:让一个hash类型key的字段值自增并指定步长
HSETNX:添加一个hash类型的key的field值,前提是这个field不存在
List类型常用命令
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
特征也与LinkedList类似:
- 有序、元素可以重复,插入和删除快,查询速度一般
LPUSH、RPUSH
LPOP、RPOP、BLPOP、BRPOP(在没有元素时等待指定时间)
LRANGE:返回一段角标范围内的所有元素
Set类型常用命令
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap
具备与HashSet类似的特征:
- 无序、元素不可重复,查找快,支持交集、并集、差集等功能
SADD:向set中添加一个或多个元素
SREM:移除set中的指定的一个或多个元素
SCARD:返回set中元素的个数
SISMEMBER:判断一个元素是否存在于set中
SMEMBERS:获取set中的所有元素
SINTER:求交集
SortedSet类型常用命令
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。
SortedSet具备下列特性:
- 可排序,元素不重复,查询速度快
ZADD:添加一个或多个元素到sorted set ,如果已经存在则更新其score值
ZREM:删除sorted set中的一个指定元素
ZSCORE:获取sorted set中的指定元素的score值
ZRANK:获取sorted set 中的指定元素的排名
ZCARD:获取sorted set中的元素个数
ZCOUNT:统计score值在给定范围内的所有元素的个数
ZINCRBY:让sorted set中的指定元素按照指定步长自增
ZRANGE:获取指定排名范围内的元素
ZRANGEBYSCORE:获取指定score范围内的元素
ZDIFF、ZINTER、ZUNION:求差集、交集、并集
以上所有排序都是按照score升序,需要降序使用ZREV开头的对应指令
SpringDataRedis
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的 类型中,官网地址:https://spring.io/projects/spring-data-redis:

默认采用的JDK序列化,可以通过配置序列化器的方式使用JSON序列化(一般来说我喜欢直接用StringRedisTemplate,使用的时候自己转JSON字符串)
1 |
|
其他类型
GEO
GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据 经纬度来检索数据,常见的命令如下:
GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
GEODIST:计算指定的两个点之间的距离并返回
GEOHASH:将指定member的坐标转为hash字符串形式并返回
GEOPOS:返回指定member的坐标
GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6以后已废弃
GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
BitMap
位图(BitMap),BitMap常见的操作命令如下:
SETBIT:向指定位置(offset)存入一个0或1
GETBIT :获取指定位置(offset)的bit值
BITCOUNT :统计BitMap中值为1的bit位的数量
BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回
BITOP :将多个BitMap的结果做位运算(与 、或、异或)
BITPOS :查找bit数组中指定范围内第一个0或1出现的位置
HyperLogLog
HyperLogLog是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,Redis中的HyperLogLog是基于string结构实现的,单个HyperLogLog的内存永远小于16kb,作为代价,其测量结果是概率性的,有小于0.81%的误差。
HyperLogLog可以用来统计UV(Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。)
Redis使用的常见问题
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗,可能造成短期的不一致
布隆过滤器
优点:内存占用较少
缺点:实现复杂,存在误判可能,不可删除(支持删除操作需要使用其他数据结构或采用特殊的设计,如计数布隆过滤器)
其他解决方案:限流、请求验证
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数 据库带来巨大的冲击。
常见的解决方案有两种:
互斥锁 + 双重校验,缺点:性能较差
互斥锁 + 逻辑过期,缺点:构建完缓存之前返回的都是脏数据
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给业务添加多级缓存
更新策略(双写一致性)
先操作数据库,再删除缓存
先写数据库再删除缓存的策略可以有效避免数据不一致的问题,但在极端情况下仍有可能出现问题。

- 延迟双删
Redisson分布式锁框架
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
可重入锁原理
采用hash结构用来存储锁,其中key表示表示这把锁是否存在,field表示当前这把锁被哪个线程持有,value记录重入次数,抢锁失败会进行重试,有个while( true) 再次进行tryAcquire进行抢锁,如果没有传入超时时间则抢锁的超时时间默认是看门狗时间(30秒)。

看门狗机制,默认情况下,Redisson 会每隔一定时间(默认为锁的超时时间的三分之一)刷新锁的过期时间,当线程执行完毕并释放锁时,看门狗机制会停止自动续约。如果线程因异常而终止,Redisson 也会尝试释放锁并停止续约。
Redis持久化
Redis有两种持久化方案:RDB持久化、AOF持久化。
RDB持久化
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录 到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
备份时机
执行save命令:save命令会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞
执行bgsave命令:这个命令执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。
Redis停机时:Redis停机时会执行一次save命令,实现RDB持久化。
触发RDB条件时:Redis内部有触发RDB的机制,可以在redis.conf文件中找到
1 | # 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB |
其他配置
1 | # 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱 |
原理
bgsave开始时会fork主进程得到子进程(fork过程当中Redis主进程会阻塞),子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。用新RDB文件替换旧的RDB文件。
fork采用的是copy-on-write技术(技术的核心思想是在真正需要修改内存时才进行复制而不是一开始就复制整个内存空间):
当主进程执行读操作时,访问共享内存
当主进程执行写操作时,则会拷贝一份数据,执行写操作,
在拷贝数据时会导致Redis主进程阻塞

缺点
RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险。
bgsave可能会导致短时间内内存使用量翻倍。
AOF持久化
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
相关配置
1 | # AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF |

AOF重写
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
AOF重写过程是由后台子进程 来完成的,这么做可以达到两个好处:
- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程,在重写期间通过
AOF 重写缓冲区确保AOF 重写过程中数据不丢失(在 AOF 重写过程中,新的写入命令会被写入 AOF 重写缓冲区,这些命令会在 AOF 重写完成后被追加到新的 AOF 文件中); - 使用子进程而不是线程,父子进程已只读的方式共享内存,当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全(与RDB一致)。
RDB和AOF的对比

混合持久化
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。AOF 优点是丢失数据少,但是数据恢复不快。为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
Redis集群
主从集群
主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。主从服务器之间的命令复制是异步进行的。
主从同步流程
- 第一次同步,全量同步,通过Replication ID判断是不是第一次同步

- 增量同步
通过repl_backlog的offset进行增量同步
repl_backlog是一个环形数组,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset。
slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。
如果slave出现网络阻塞,未同步的数据超过了repl_baklog的容量就需要进行全量同步了


主从同步优化
在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
Redis哨兵集群
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。
原理
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
故障恢复(哨兵之间投票选取进行故障恢复的哨兵)
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据:
- 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
- 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高
切换master:
- sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
- sentinel给所有其它slave发送slaveof命令,让这些slave成为新master的从节点,开始从新的master上同步数据
- 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:海量数据存储问题、高并发写的问题。
分片集群特征:
集群中有多个master,每个master保存不同数据
每个master都可以有多个slave节点
master之间通过ping监测彼此健康状态(自带哨兵功能)
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
原理
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,计算方式:
key中包含”{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分
key中不包含“{}”,整个key都是有效部分
根据key的有效部分计算哈希值,对16384取余,再根据哈希槽的分配,找插槽所在实例即可。
集群脑裂
集群脑裂就像一个集群被分为了两部分,同时有两个大脑(主节点)。
在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。
如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据,此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。
这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了,此时脑裂出现了。
然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点,然后旧主节点会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的旧主节点会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在旧主节点 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
解决方案
核心思想是当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
当主节点与从节点之间发生网络时,停止了该主节点提供的服务,在哨兵进行故障转移后,由新的主节点提供服务。
在 Redis 的配置文件中有两个参数我们可以设置:
- min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
- min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。
最佳实践(TODO)
Redis原理
数据结构
动态字符串
Redis中保存的Key是字符串,value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。
不过Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题:
获取字符串长度的需要通过运算
不可修改
Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS。思想类似于Java当中的ArrayList,预先分配一些内存,同时记录存储的真实数据的大小。
Redis是C语言实现的,其中SDS是一个结构体,源码如下:

当空间不足时会重新申请内存进行扩容:
如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1
如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。
其他TODO
网络模型TODO
线程模型
很多人都听说过Redis是单线程的,这其实指的是接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端这个过程是由一个线程(主线程)来完成的(即执行用户命令的线程),实际上Redis是会启动多个线程的。
像之前提到的数据持久化和接下来要提到的过期key的处理,都是有专门的线程来负责的。
Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会创建以下线程:
- Redis-server : Redis的主线程,主要负责执行命令;
- bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
- io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。
执行用户命令采用单线程的核心原因是CPU 并不是制约 Redis 性能表现的瓶颈所在,并且采用单线程的设计可以保证所有操作的顺序执行,从而避免了并发带来的数据不一致问题和死锁问题。
Redis内存回收
过期key处理
Redis本身是一个典型的key-value内存存储数据库,因此所有的key、value都保存在Dict结构中。不过在其database结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL。
过期key的删除方式:
惰性删除
并不是在TTL到期后就立刻删除,而是在访问一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
周期删除
定期执行一个后台任务来检查并删除过期的键。这个任务是通过 Redis 的定时器机制(定时器机制会定期触发一系列任务)来调度的,通常每秒执行多次(具体频率由配置参数 hz 控制,默认值为 10,即每秒运行 10 次)每次定期任务执行时,Redis 会随机选取一定数量的数据库,并从中随机。选取一定数量的键进行检查。如果键已过期,则将其删除。
分为SLOW和FAST模式,慢速模式允许每个处理周期花费一定比例的时间来检查过期键(默认为 25%)。当慢速模式不足以处理所有过期键时(如在慢速模式中发现过期键的概率较高),会触发快速模式,允许每个处理周期花费更多的时间占比来检查过期键增加检查过期键的频率。
内存淘汰策略
当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。
Redis支持8种不同策略来选择要删除的key:
noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选。
allkeys-lru: 对全体key,基于LRU算法进行淘汰
allkeys-lfu: 对全体key,基于LFU算法进行淘汰
volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。
volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
LRU:最近最久未用;LFU:最近最少使用,会统计访问次数(非精确统计)
部分内容转载自黑马程序员