【Redis】Redis 基础

Redis 简介
Redis:REmote DIctionary Server(远程字典服务器) 是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器。
Redis 知识全景图:

“两大维度”就是指系统维度和应用维度,“三大主线”也就是指高性能、高可靠和高可扩展(可以简称为“三高”)。
Redis 单线程模型
Redis 采用单线程模型来处理客户端的请求。对读写等事件的响应是通过对epoll函数的包装来做到的。Redis的实际处理速度完全依靠主进程的执行效率。
Epoll是Linux内核为处理大批量文件描述符而作了改进的epoll,是Linux下多路复用IO接口select/poll的增强版本, 它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
Redis是单线程 + 多路IO复用技术
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字(监听 socket 请求)和已连接套接字(已连接后的 socket 发送读写请求)。内核会一直监听这些套接字上的连接请求或数据(读写)请求。一旦有请求到达,就会通知 Redis 主线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
下图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能通知到 Redis 线程,select/epoll提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。
以连接请求和读数据请求为例:这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。
串行 vs 多线程+锁(memcached) vs 单线程 + 多路IO复用(Redis)
(与Memcache三点不同:支持多数据类型,支持持久化,单线程+多路IO复用)
Redis 特点
Redis 与其他 key - value 缓存软件有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
- Redis支持数据的备份,即master-slave模式的数据备份
Redis 能够用来:
- 内存存储和持久化:redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务
- 取最新N个数据的操作,如:可以将最新的10条评论的ID放在Redis的List集合里面
- 模拟类似于HttpSession这种需要设定过期时间的功能
- 发布、订阅消息系统
- 定时器、计数器
Redis 安装见 Linux 开发环境配置文档
Redis 五大数据类型
- String(字符串)
- String是Redis最基本的类型,可以理解成与Memcached一模一样的类型,一个key对应一个value。
- String类型是二进制安全的。意思是Redis的String可以包含任何数据。比如jpg图片或者序列化的对象。
- String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
- Hash(哈希,类似java里的Map)
- Redis Hash 是一个键值对集合。
- Redis Hash是一个String类型的field和value的映射表,Hash特别适合用于存储对象。
类似Java里面的Map<String,Object>
- List(列表)
- Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。
- 它的底层实际是个链表
- Set(集合)
- Redis的Set是String类型的无序集合。它是通过
HashTable实现实现的
- Redis的Set是String类型的无序集合。它是通过
- Zset(sorted set:有序集合)
- Redis Zset 和 Set 一样也是String类型元素的集合,且不允许重复的成员。
- 不同的是每个元素都会关联一个double类型的分数。
- Redis正是通过分数来为集合中的成员进行从小到大的排序。Zset的成员是唯一的,但分数(score)却可以重复。
Redis 常见数据类型操作命令:
String
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
List
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

List的数据结构为快速链表quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
Set
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都指向同一个内部值。
Hash
Hash 是一个键值对集合。它是一个String类型的field和value的映射表,Hash特别适合用于存储对象。它类似Java里面的Map<String,Map<K,V>>,存储方式:

通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
Zset
Redis有序集合Zset与普通集合Set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的,所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
Zset底层使用了两个数据结构
- Hash,Hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
- 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
跳跃表示例:

实际应用场景
String
- 商品编号、订单号采用INCR命令生成
- 是否喜欢的文章
Hash
- 新增商品
hset shopcar:uid1024 334488 1 - 新增商品
hset shopcar:uid1024 334477 1 - 增加商品数量
hincrby shopcar:uid1024 334477 1 - 商品总数
hlen shopcar:uid1024 - 全部选择
hgetall shopcar:uid1024
List
A和B发布了文章分别是11和22。
- 我关注了A和B,只要他们发布了新文章,就会安装进我的List:
lpush likearticle:myid1122 - 查看我自己的号订阅的全部文章,类似分页,下面0~10就是一次显示10条:
lrange likearticle:myid 0 10
Set
- 微信抽奖小程序
- 用户ID,立即参与按钮
SADD key 用户ID
- 显示已经有多少人参与了、上图23208人参加
SCARD key
- 抽奖(从set中任意选取N个中奖人)
SRANDMEMBER key 2(随机抽奖2个人,元素不删除)SPOP key 3(随机抽奖3个人,元素会删除)
- 用户ID,立即参与按钮
- 微信朋友圈点赞
- 新增点赞
sadd pub:msglD 点赞用户ID1 点赞用户ID2
- 取消点赞
srem pub:msglD 点赞用户ID
- 展现所有点赞过的用户
SMEMBERS pub:msglD
- 点赞用户数统计,就是常见的点赞红色数字
scard pub:msgID
- 判断某个朋友是否对楼主点赞过
SISMEMBER pub:msglD用户ID
- 新增点赞
- 微博好友关注社交关系
- 共同关注:我去到局座张召忠的微博,马上获得我和局座共同关注的人
sadd s1 1 2 3 4 5sadd s2 3 4 5 6 7SINTER s1 s2
- 我关注的人也关注他(大家爱好相同)
- 共同关注:我去到局座张召忠的微博,马上获得我和局座共同关注的人
- QQ内推可能认识的人
sadd s1 1 2 3 4 5sadd s2 3 4 5 6 7SINTER s1 s2SDIFF s1 s2SDIFF s2 s1
Zset
- 根据商品销售对商品进行排序显示
- 定义商品销售排行榜(sorted set集合),key为goods:sellsort,分数为商品销售数量。
- 商品编号1001的销量是9,商品编号1002的销量是15
- zadd goods:sellsort 9 1001 15 1002 - 有一个客户又买了2件商品1001,商品编号1001销量加2 -
zincrby goods:sellsort 2 1001 - 求商品销量前10名 -
ZRANGE goods:sellsort 0 10 withscores
- 抖音热搜
- 点击视频
ZINCRBY hotvcr:20200919 1 八佰ZINCRBY hotvcr:20200919 15 八佰 2 花木兰
- 展示当日排行前10条
ZREVRANGE hotvcr:20200919 0 9 withscores
- 点击视频
Redis 新数据类型
Bitmaps
现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图

合理地使用操作位能够有效地提高内存使用率和开发效率。
Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
- Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
- Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

Bitmaps可用于统计网站的活跃用户数量,占用内存远小于Set。
Bitmaps常用命令见 Redis6 文档。
HyperLogLog
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。解决基数问题有很多种方案:
- 数据存储在MySQL表中,使用distinct count计算不重复个数
- 使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
HyperLogLog常用命令见 Redis6 文档。
Geospatial
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。Redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
Geospatial常用命令见 Redis6 文档。
Redis 常用命令
Key 关键字

String

常用命令:
SET key valueGET keyMSET key value [key value…]同时设置多个键值MGET key [key…]同时获取多个键值INCR key递增数字 (可以不用预先设置key的数值。如果预先设置key但值不是数字,则会报错)INCRBY key increment增加指定的整数DECR key递减数值DECRBY key decrement减少指定的整数STRLEN key获取字符串长度SETNX key value分布式锁
List

常用命令:
LPUSH key value [value …]向列表左边添加元素RPUSH key value [value …]向列表右边添加元素LRANGE key start stop查看列表LLEN key获取列表中元素的个数
性能总结:
- 它是一个字符串链表,left、right都可以插入添加;
- 如果键不存在,创建新的链表;
- 如果键已存在,新增内容;
- 如果值全移除,对应的键也就消失了。
- 链表的操作无论是头和尾效率都极高,但假如是对中间元素进行操作,效率就很惨淡了。
Set

常用命令:
SADD key member [member …]添加元素SREM key member [member …]删除元素SMEMBERS key获取集合中的所有元素SISMEMBER key member判断元素是否在集合中SCARD key获取集合中的元素个数SRANDMEMBER key [数字]从集合中随机弹出一个元素,元素不删除SPOP key[数字]从集合中随机弹出一个元素,出一个删一个
集合运算:
- 集合的差集运算A - B
- 属于A但不属于B的元素构成的集合
SDIFF key [key …]
- 集合的交集运算A ∩ B
- 属于A同时也属于B的共同拥有的元素构成的集合
SINTER key [key …]
- 集合的并集运算A U B
- 属于A或者属于B的元素合并后的集合
SUNION key [key …]
Hash

常用命令:
HSET key field value一次设置一个字段值HGET key field一次获取一个字段值HMSET key field value [field value …]一次设置多个字段值HMGET key field [field …]一次获取多个字段值HGETALL key获取所有字段值HLEN key获取某个key内的全部数量HDEL key删除一个key
ZSet

常用命令:
ZADD key score member [score member …]添加元素ZRANGE key start stop [WITHSCORES]按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素ZSCORE key member获取元素的分数ZREM key member [member …]删除元素ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]获取指定分数范围的元素ZINCRBY key increment member增加某个元素的分数ZCARD key获取集合中元素的数量ZCOUNT key min max获得指定分数范围内的元素个数ZREMRANGEBYRANK key start stop按照排名范围删除元素- 获取元素的排名
ZRANK key member从小到大ZREVRANK key member从大到小
Redis 配置文件介绍
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf可以通过 CONFIG 命令查看或设置配置项。
Redis CONFIG 命令格式如下:
1 | redis 127.0.0.1:6379> CONFIG GET CONFIG_SETTING_NAME |
实例
1 | redis 127.0.0.1:6379> CONFIG GET loglevel |
参数说明
redis.conf 配置项说明如下:

Redis 持久化
Redis 的两种持久化方式:
- RDB(Redis DataBase):备份数据集
- AOF(Append Only File):仅备份指令
RDB
RDB(Redis DataBase)是指在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
在Linux程序中,fork() 会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
写时复制技术:
其他线程在读取数据时,当有写操作时,先将数据复制一份出来,写操作的线程将向其内写数据。写完后将数据与原先数据进行合并。之后再来读操作时读取的就是新的内容。(将原数据的引用指向复制后的数据上)
RDB持久化过程:

RDB 默认保存的备份文件为 dump.rdb 文件(可以在配置文件中修改名称)。相关配置在 redis.conf 的 ### SNAPSHOTTING ### 部分。dump.rdb文件的保存路径在配置文件的 dir ./ 位置配置。
如何触发RDB快照:
- 配置文件中默认的快照配置
dbfilename dump.rdb - 命令
save或者是bgsavesave:同步保存,阻塞其他所有操作bgsave:Redis会在后台(Background)异步进行快照操作, 快照同时还可以响应客户端请求。可以通过lastsave命令获取最后一次成功执行快照的时间
- 执行
flushall命令,也会产生dump.rdb文件,但里面是空的,无意义
如何恢复:
- 将备份文件
dump.rdb移动到 Redis 安装目录并启动服务即可 CONFIG GET dir获取备份文件保存的目录
如何停止:
动态停止RDB保存规则的方法:redis-cli config set save ""
RDB 常见配置
dump.rdb文件的保存路径:

保存时机策略:

默认保存时机策略是:1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。若想禁用保存,则可以不设置save指令或给save传入空字符串。
stop-writes-on-bgsave-error:

当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes。
rdbcompression 压缩文件:

对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes。
rdbchecksum 检查完整性:

在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。推荐yes。
RDB 优势与劣势
优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高
- RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。
劣势
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
RDB 小结
- RDB是一个非常紧凑的文件。
- RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。
- 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一一些。
- 数据丢失风险大。
- RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候fork的过程是非常耗时的吗,可能会导致Redis在一些毫秒级不能回应客户端请求。
AOF
AOF(Append Only File)以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加(append)文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。AOF的同步是异步完成的。
AOF和RDB同时开启时,系统默认读取AOF的数据(数据不会存在丢失)
AOF 持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区内;
- AOF缓冲区根据AOF 持久化策略[always,everysec,no] 将操作sync同步到磁盘的AOF文件中;
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
- Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

RDB 默认保存的备份文件为 appendonly.aof 文件(可以在配置文件中修改名称)。相关配置在 redis.conf 的 ### APPEND ONLY MODE ### 部分。AOF文件的保存路径,同RDB的路径一致。
AOF和RDB同时开启时,系统默认读取AOF的数据(数据不会存在丢失)
若 appendonly.aof 文件出现损坏,可以使用Redis-check-aof --fix命令进行修复。
AOF 同步频率设置
- appendfsync always:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
- appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失(RDB方式损失的时间可能更长)。
- appendfsync no:Redis不主动进行同步,把同步时机交给操作系统。
Rewrite 重写压缩
AOF采用文件追加方式,文件会越来越大。为避免出现此种情况,新增了重写机制Rewrite:当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集。可以使用命令 bgrewriteaof:在一个子进程中进行aof的重写,从而不阻塞主进程对其余命令的处理,同时解决了aof文件过大问题。
Rewrite 原理
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,实质上就是把 rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
每次 rewrite 并不是基于旧的指令日志进行 merge 的(因为可能某次宕机导致日志中存储错误的指令),而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
Rewrite 相关配置
no-appendfsync-on-rewrite
同时在执行bgrewriteaof操作和主进程写aof文件的操作,两者都会操作磁盘,而bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候出现阻塞的情形,现在no-appendfsync-on-rewrite参数出场了。
如果该参数设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题。如果设置为yes呢?这就相当于将appendfsync设置为no,这说明并没有执行磁盘操作,只是写入了缓冲区,因此这样并不会造成阻塞(因为没有竞争磁盘),但是如果这个时候redis挂掉,就会丢失数据。丢失多少数据呢?在linux的操作系统的默认设置下,最多会丢失30s的数据。
- 如果
no-appendfsync-on-rewrite=yes:正在重写时新增命令不再写入aof文件,而是写入缓存,等重写结束再同步到aof文件。这种模式下用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能) - 如果
no-appendfsync-on-rewrite=no:正在重写时新增命令会阻塞等待重写操作完成再同步到磁盘中,缺点是会发生阻塞。(数据安全,但是性能降低)
重写触发机制:
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的两倍且文件大于64M时触发。修改以下配置即可更改重写触发时机:
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MBauto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写,一般该值设置大一些 3~5GB。
Rewrite 流程
bgrewriteaof触发重写,判断是否当前有bgsave(RDB进程)或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。- 主进程fork出子进程执行重写操作,保证主进程不会阻塞。
- 子进程遍历Redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
- 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
- 主进程把
aof_rewrite_buf中的数据写入到新的AOF文件。 - 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

AOF 优势与劣势
优势
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
劣势
- 相同数据集的数据而言aof文件占用磁盘空间要远大于rdb文件,恢复速度慢于rdb
- 每次读写都同步的话,有一定的性能压力。
- AOF运行效率要慢于RDB,每秒同步策略效率较好,不同步效率和RDB相同
AOF 小结
- AOF文件时一个只进行追加的日志文件
- Redis可以在AOF文件体积变得过大时,自动地在后台对AOF进行重写
- AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以Redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松
- 对于相同的数据集来说,AOF文件的体积通常要大于RDB文件的体积
- 根据所使用的 fsync 策略,AOF的速度可能会慢于RDB
混合使用 AOF 和 RDB
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。

这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势。
总结
官方推荐两个都启用。
- 如果对数据不敏感,可以选单独用RDB。
- 不建议单独用 AOF,因为可能会出现Bug。
- 如果只是做纯内存缓存,可以都不用。
官方建议:
- RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
- AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾
- Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
- 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式
同时开启两种持久化方式:
- 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
- RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢? 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
性能建议:
- 因为RDB文件只用作后备用途,建议只在 Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
- 如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价:一是带来了持续的IO,二是AOF Rewrite的最后将Rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。
- 只要硬盘许可,应该尽量减少AOF Rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。
- 默认超过原大小100%大小时重写可以改到适当的数值。
三点建议:
- 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
- 如果允许分钟级别的数据丢失,可以只使用 RDB;
- 如果只用 AOF,优先使用
everysec的配置选项,因为它在可靠性和性能之间取了一个平衡。
Redis 发布和订阅
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。

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

Redis Sentinel 间通讯时使用了发布订阅功能。
Redis 事务
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
事务常用命令

Multi、Exec、Discard
从输入Multi命令开始(组队阶段),输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec命令后进入执行阶段,Redis会将之前命令队列中的命令依次执行。组队的过程中可以通过Discard命令来放弃组队。

- 当组队阶段中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

- 当执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

事务三阶段
- 开启:以
MULTI开始一个事务 - 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
- 执行:由
EXEC命令触发事务
WATCH
WATCH指令,类似乐观锁,事务提交时,如果Key的值已被别的客户端改变, 比如某个list已被别的客户端push/pop过了,整个事务队列都不会被执行。
通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化, EXEC命令执行的事务都将被放弃,同时返回Nullmulti-bulk应答以通知调用者事务执行失败
悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。
此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
使用 WATCH 乐观锁 + LUA脚本 可以解决超卖问题和库存遗留问题。详细代码见见 Redis6 文档。
Redis 事务三特性
- 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
- 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行, 也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题
- 不保证原子性:Redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
Redis 主从复制
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。主从复制能够用来:
- 读写分离,性能扩展
- 容灾快速恢复

每次与Master断开连接之后,都需要重新连接,除非配置进redis.conf文件(具体位置:redis.conf搜寻#### REPLICATION ####)。常用命令:
- 从库配置命令:
slaveof 主库IP 主库端口。 - 查看当前主从配置:
info replication
replication
英 [ˌreplɪ’keɪʃ(ə)n] 美 [ˌreplɪ’keɪʃ(ə)n]
n.
(绘画等的)复制;拷贝;重复(实验);(尤指对答辩的)回答
常见问题
- 切入点问题?slave1、slave2是从头开始复制还是从切入点开始复制?比如从k4进来,那之前的123是否也可以复制?答:从头开始复制;123也可以复制
- 从机是否可以写?set可否?答:从机不可写,不可set,主机可写
- 主机shutdown后情况如何?从机是上位还是原地待命答:从机还是原地待命
- 主机又回来了后,主机新增记录,从机还能否顺利复制?答:能
- 其中一台从机down后情况如何?依照原有它能跟上大部队吗?答:不能跟上,每次与master断开之后,都需要重新连接,除非你配置进
redis.conf文件(具体位置:redis.conf搜寻#### REPLICATION ####)
薪火相传
上一个slave可以是下一个slave的Master,slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力,去中心化降低风险。风险是一旦某个slave宕机,后面的slave都没法备份。主机挂了,从机还是从机,无法写数据了。中途变更转向:会清除之前的数据,重新建立拷贝最新的。

反客为主
当一个master宕机后,后面的slave可以立刻升为master,使当前数据库停止与其他数据库的同步,转成主数据库。其后面的slave不用做任何修改。
使用命令:SLAVEOF no one
主从复制原理
slave启动成功连接到master后会发送一个sync命令。master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个RDB数据文件到slave,以完成一次完全同步。
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:master继续将新的所有收集到的修改命令依次传给slave(只传送新增的修改命令),完成同步
在Redis2.8版本后,主从断线后恢复的情况下实现增量复制。

Redis 哨兵模式(sentinel)
哨兵模式是反客为主的自动版,能够在后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。通常哨兵也配置多个,互相监控。一组sentinel能同时监控多个master,原master重启后会变为从机。哨兵与服务器间通过发布订阅获得消息。
Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance) 该系统执行以下三个任务:
- 监控(Monitoring):Sentinel 会不断地定期检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification):当被监控的某个 Redis 服务器出现问题时,Sentinel 可以通过 API 向管理员或者其他应用程序发送通知(例如发邮件)。
- 自动故障迁移(Automaticfailover):当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器;通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols) 来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。
哨兵是 Redis 集群架构中非常重要的一个组件,主要功能如下:
- 集群监控,负责监控 Redis Master 和 Slave 进程是否正常工作;
- 消息通知,如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员;
- 故障转移,如果 Master node 挂掉了,会自动转移到 Slave node 上;
- 配置中心,如果故障转移发生了,通知 Client 客户端新的 Master 地址。


假设主服务器宕机,哨兵1先监测到这个结果,系统并不会立刻进行 failover(故障转移) 过程,仅仅是哨兵1主观地认为其不可用,此现象称为主观下线。当其后的哨兵也检测到主服务器不可用,并且数量达到一定值时(该数量在配置文件中配置,见下文),那么哨兵之间将进行一次投票,选出某一个哨兵发出 failover 指令,根据优先级/偏移量选出新的主机,所有从机都设置该服务器为主服务器,这个过程被称为客观下线。
主观下线:所谓主观下线,就是单个sentinel认为某个服务下线(有可能是接收不到订阅,之间的网络不通等等原因)。
sentinel会以每秒一次的频率向所有与其建立了命令连接的实例(master,从服务,其他sentinel)发ping命令,通过判断ping回复是有效回复,还是无效回复来判断实例时候在线(对该sentinel来说是“主观在线”)。
sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度,如果实例在down-after-milliseconds毫秒内,返回的都是无效回复,那么sentinel会认为该实例已(主观)下线,修改其flags状态为SRI_S_DOWN。如果多个sentinel监视一个服务,有可能存在多个sentinel的down-after-milliseconds配置不同,这个在实际生产中要注意。
客观下线:当主观下线的节点是主节点时,此时该哨兵3节点会通过指令sentinel is-masterdown-by-addr寻求其它哨兵节点对主节点的判断,如果其他的哨兵也认为主节点主观线下了,则当认为主观下线的票数超过了quorum(选举)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了,大部分哨兵节点都同意下线操作,也就说是客观下线。

哨兵至少需要3个实例,来保证自己的健壮性。哨兵+redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性。对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充分的测试和演练。
自动故障转移机制
哪个从机会被选举为主机呢?首先判断每个slave与master断开连接的次数:如果一个slave与master失去联系超过10次,并且每次都超过了配置的最大失联时间(down-after-milliseconds),如果sentinel在进行failover时发现slave失联,那么这个slave就会被sentinel认为不适合用来做新master的。
符合上述条件的slave才会被列入master候选人列表,并根据以下顺序来进行排序:

- 根据优先级别:
slave-priority(在配置文件中配置slave-priority设置)。优先级默认:slave-priority 100,值越小优先级越高(如果一个redis的slave优先级配置为0,那么它将永远不会被选为master。但是它依然会从master哪里复制数据。) - 偏移量是指与原主机数据相比相差最少的,即同步率最高的(根据复制的下标数比较谁的次数多)
- 每个Redis实例启动后都会随机生成一个40位的
runid
配置方法
- 新建
sentinel.conf文件 - 配置监控的master地址:
sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量(可以同时配置多个哨兵一起监控,此处配置的1代表至少1个哨兵觉得主服务器宕机才可以进行重新选举)。
- 启动哨兵:
redis-sentinel ./sentinel.conf
复制延时
由于所有的写操作都是先在master上操作,然后同步更新到slave上,所以从master同步到slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,slave机器数量的增加也会使这个问题更加严重。
哨兵 leader 选举算法
如果主节点被判定为客观下线之后,就要选取一个哨兵节点来完成后面的故障转移工作,选举出一个leader的流程如下:
- 每个在线的哨兵节点都可以成为领导者,当它确认(比如哨兵3)主节点下线时,会向其它哨兵发
is-master-down-by-addr命令,征求判断并要求将自己设置为领导者,由领导者处理故障转移; - 当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;
- 如果哨兵3发现自己在选举的票数大于等于
num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举…

Redis 哨兵主备切换的数据丢失问题
共有两种数据丢失场景:
1. 异步复制时间过长
因为master->slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了(此时异步复制时间过长),这些数据就丢失了。
2. 脑裂
脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,这个时候,集群中就会出现两个master。
此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master数据可能就会丢失。因此master在恢复的时候,会被作为一个slave挂到新的master上,自己的数据会被清空,从新的master复制数据,导致刚才客户端传来的数据丢失。
解决异步复制和脑裂导致的数据丢失
设置数据复制和同步的延迟时间,当slave与master间的数据复制同步时间超过了延迟时间,就拒绝客户端的写请求:
1 | min-slaves-to-write 1 # 最少一个slave和master进行数据复制同步时超过延迟时间 |
lag:落后,即延迟了10s
该配置要求至少有1个slave进行数据复制和同步的延迟不能超过10秒。如果一旦某个slave和master进行数据复制和同步的延迟超过了10秒钟,那么这个时候,master就不会再接收任何请求了(即,若slave和master数据同步时间太长,master就别再写数据了,让客户端等待稍后再写)。
1. 减少异步复制的数据丢失
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求(让客户端稍后再写),这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内。
2. 减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求(让客户端稍后再写)。
这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失。上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求。因此在脑裂场景下,最多就丢失10秒的数据。
总结
哨兵架构,几乎可以做到了我们的要实现的高可用,但是哨兵的选举还是需要时间的,而且中间会阻塞客户端的请求,假如我们的选举消耗了1秒(实际可能几秒,高则几十秒),就在这1秒的时候来了客户端的请求,那个请求也是不可用的,并且我们的读写的节点实际还是单节点的,怎么办? 使用 Redis集群架构:

Redis的集群其实就是一个个小的主从结合在一起(官方建议小于1000个小主从),变成了我们的Redis集群,每个小主从也就是我们的Redis数据分片。
Redis Cluster 集群
问题引出
- 容量不够,redis如何进行扩容?
- 并发写操作, redis如何分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
之前通过代理主机来解决,但是 Redis 3.0 中提供了解决方案。就是无中心化集群配置。
什么是集群
Redis 集群实现了对Redis的水平扩容,即启动N个Redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的 1/N。Redis 的集群的功能就是为了解决单机 Redis 容量有限的问题。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
Redis Cluster 可以说是 Redis Sentinel 带分片的加强版。也可以说:
- Redis Sentinel 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master ,继续提供服务。
- Redis Cluster 着眼于扩展性,在单个 Redis 内存不足时,使用 Cluster 进行分片存储。
Redis 集群配置方法见 Redis6 文档。
slots 哈希槽
一个 Redis 集群包含 16384 个哈希槽(hash slot), 数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
为什么是 16384 呢?主要考虑集群内的网络带宽,而 16384 刚好是 2K 字节大小。
集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
- 节点 A 负责处理 0 号至 5460 号插槽。
- 节点 B 负责处理 5461 号至 10922 号插槽。
- 节点 C 负责处理 10923 号至 16383 号插槽。
在redis-cli每次录入、查询键值,Redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,Redis会报错,并告知应前往的Redis实例地址和端口。
redis-cli客户端提供了 –c 参数实现自动重定向:如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。
不在一个 slot 下的键值,是不能使用 mget, mset 等多键操作。

可以通过 {} 来定义组的概念,从而使key中 {} 内相同内容的键值对放到一个slot中去。

查询集群中的值
CLUSTER GETKEYSINSLOT <slot><count> 返回 count 个 slot 槽中的键。
故障恢复
如果主节点下线?从节点能否自动升为主节点?注意:15秒超时

主节点恢复后,主从关系会如何?主节点回来变成从机。

如果某一段插槽的所有主从节点都宕掉,Redis服务是否还能继续?
- 如果某一段插槽的所有主从都挂掉,而
cluster-require-full-coverage为 yes ,那么 ,整个集群都挂掉 - 如果某一段插槽的所有主从都挂掉,而
cluster-require-full-coverage为 no ,那么,该插槽数据全都不能使用,也无法存储,但其他插槽仍然可以使用。
其中 cluster-require-full-coverage 为 redis.conf中的参数。
集群优点
- 无中心架构:访问任何一台服务器的主机都能路由到指定的服务器上,无需单独配置一台服务器进行路由;
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布;
- 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除;
- 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升;
- 降低运维成本,提高系统的扩展性和可用性。
集群缺点
- Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅 JedisCluster 相对成熟,异常处理部分还不完善,比如常见的 “max redirect exception”。
- 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
- 数据通过异步复制,不保证数据的强一致性。
- 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
- Slave 在集群中充当 “冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 Slave 资源的利用率。
Redis 应用问题解决
缓存穿透
大量的请求瞬时涌入系统,而这个数据在 Redis 中不存在,所有的请求都落到了数据库上,从而可能压垮数据源。例如反复用一个不存在的用户id进行访问,会对数据库进行大量查询。造成这种情况的原因有系统设计不合理、缓存数据更新不及时,或爬虫等恶意攻击。

解决办法:
1. 使用布隆过滤器
布隆过滤器是一种比较巧妙的概率型数据结构,它实际上是一个很长的二进制向量 bitmaps 和一系列随机映射函数(哈希函数)。
详细介绍见博客:https://zhuanlan.zhihu.com/p/43263751
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
其思想是:将查询的参数都存储到一个 bitmaps 中,在查询缓存前,再找个新的 bitmap,在里面对参数进行验证。如果验证的 bitmaps 中存在,则进行底层缓存的数据查询,如果 bitmap 中不存在查询参数,则进行拦截,不再进行缓存的数据查询。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
2. 缓存空对象
如果从数据库查询的结果为空,依然把这个结果进行缓存,那么当用 key 获取数据时,即使数据不存在,Redis 也可以直接返回结果,避免多次访问数据库。但是缓存空值的缺点是:
- 如果存在黑客恶意的随机访问,造成缓存过多的空值,那么可能造成很多内存空间的浪费。但是也可以对这些数据设置很短的过期时间来控制;
- 如果查询的 key 对应的 Redis 缓存空值没有过期,数据库这时有了新数据,那么会出现数据库和缓存数据不一致的问题。但是可以保证当数据库有数据后更新缓存进行解决。
3. 设置可访问的名单(白名单)
使用 bitmaps 类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
4. 进行实时监控
当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
缓存击穿
缓存击穿是指一个key非常热点(其内容存在于数据库中,不像缓存穿透里key的内容不存在),但某个时刻,其在Redis中过期,这时大量且持久的并发集中对数据库中的这个key进行访问,就会瞬间压垮数据库,就像在屏幕上凿开一个洞,击穿了数据库。常见场景:微博突发热点,大量高并发请求瞬间访问该热点key,但该key在某个时刻过期,其过期瞬间这些高并发请求就会一起访问数据库,导致其被击穿。

解决方法:
1. 预先设置热门数据
在Redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
2. 实时调整
现场监控哪些数据热门,实时调整key的过期时长
3. 使用分布式锁
使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可,不会再访问数据库。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。Redis 分布式锁使用方法见文章【Redis】Redis 分布式锁
在缓存失效的时候(判断拿出来的值为空),不是立即去访问数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key。
- 当操作返回成功时,再进行访问数据库的操作,并回设缓存,最后删除mutex key;
- 当操作返回失败,证明有线程正在访问数据库,当前线程睡眠一段时间再重试整个get缓存的方法。

缓存雪崩
缓存雪崩是指当大量缓存几乎同一时间失效或Redis宕机时,大量的请求访问直接请求数据库,导致数据库服务器无法抗住请求或挂掉的情况。这时网站常常会出现 502 错误,导致网站不可用问题。
缓存雪崩与缓存击穿的区别在于这里针对很多key几乎同时过期,前者则是某一个key。
缓存失效时的雪崩效应对底层系统的冲击非常可怕,在预防缓存雪崩时,有以下方案:
1. 构建多级缓存架构
Nginx缓存 + Redis缓存 + 其他缓存(ehcache等)
2. 使用锁或队列
用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。
3. 设置过期标志更新缓存
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
4. 将缓存失效时间分散开
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
5. 服务降级
对数据库进行过载保护或应用层限流,这种情况下一般是在网站处于大流量、高并发时,服务器整体不能承受时,可以采用的一种限流保护措施;
分布式锁
Redis 分布式锁使用方法见文章【Redis】Redis 分布式锁
Jedis 使用
导入 Maven 依赖:
1 | <dependency> |
Jedis 常用 API
1 | import java.util.HashMap; |
Jedis 事务
未加锁事务:
1 | import redis.clients.jedis.Jedis; |
加锁时事务(jedis.watch("xxx")):
1 | import redis.clients.jedis.Jedis; |
Jedis 主从复制
1 | import redis.clients.jedis.Jedis; |
JedisPool
- 获取Jedis实例需要从JedisPool中获取
- 用完Jedis实例需要返还给JedisPool
- 如果Jedis在使用过程中出错,则也需要还给JedisPool
饿汉模式下的JedisPool单例创建:
1 | import redis.clients.jedis.Jedis; |
测试:
1 | import redis.clients.jedis.Jedis; |
JedisPool 配置总结
JedisPool的配置参数大部分是由JedisPoolConfig的对应项来赋值的。
- maxActive:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted。
- maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
- whenExhaustedAction:表示当pool中的jedis实例都被allocated完时,pool要采取的操作;默认有三种。
WHEN_EXHAUSTED_FAIL--> 表示无jedis实例时,直接抛出NoSuchElementException;WHEN_EXHAUSTED_BLOCK--> 则表示阻塞住,或者达到maxWait时抛出JedisConnectionException;WHEN_EXHAUSTED_GROW--> 则表示新建一个jedis实例,也就说设置的maxActive无用;
- maxWait:表示当borrow一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛JedisConnectionException;
- testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
- testOnReturn:return 一个jedis实例给pool时,是否检查连接可用性(ping());
- testWhileIdle:如果为true,表示有一个idle object evitor线程对idle object进行扫描,如果validate失败,此object会被从pool中drop掉;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
- timeBetweenEvictionRunsMillis:表示idle object evitor两次扫描之间要sleep的毫秒数;
- numTestsPerEvictionRun:表示idle object evitor每次扫描的最多的对象数;
- minEvictableIdleTimeMillis:表示一个对象至少停留在idle状态的最短时间,然后才能被idle object evitor扫描并驱逐;这一项只有在timeBetweenEvictionRunsMillis大于0时才有意义;
- softMinEvictableIdleTimeMillis:在minEvictableIdleTimeMillis基础上,加入了至少minIdle个对象已经在pool里面了。如果为-1,evicted不会根据idle time驱逐任何对象。如果minEvictableIdleTimeMillis>0,则此项设置无意义,且只有在timeBetweenEvictionRunsMillis大于0时才有意义;
- lifo:borrowObject返回对象时,是采用DEFAULT_LIFO(last in first out,即类似cache的最频繁使用队列),如果为False,则表示FIFO队列;
其中JedisPoolConfig对一些参数的默认设置如下:
- testWhileIdle=true
- minEvictableIdleTimeMills=60000
- timeBetweenEvictionRunsMillis=30000
- numTestsPerEvictionRun=-1