Redis
Redis
简介
Redis,英文全称是Remote Dictionary Server(远程字典服务) ,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
高效性能分析
存储模式
基于内存实现,而非磁盘。
数据结构
基于不同业务场景的高效数据结构。
- 动态字符串(REDIS_STRING):整数(REDIS_ENCODING_INT)、字符串(REDIS_ENCODING_EMBSTR)、字符串(REDIS_ENCODING_RAW)
- 快表(REDIS_ENCODING_QUICKLIST)
- 压缩列表(REDIS_ENCODING_ZIPLIST)
- 跳跃表(REDIS_ENCODING_SKIPLIST)
- 哈希表(REDIS_ENCODING_HT)
- 整数集合(REDIS_ENCODING_INTSET)
线程模型
Redis 的网络 IO 以及键值对指令读写是由单个线程来执行的,避免了不必要的contextswitch和竞选。
I/O 模型
基于I/O多路复用模型,非阻塞的I/O模型。
恰当的数据编码
根据实际数据类型,选择合理的数据编码。
数据结构类型
基本数据类型

| 类型 | 简介 | 使用方法 | 内部编码 | 应用场景 |
|---|---|---|---|---|
| String | 字符串 | set key value / get key | int/embstr/raw | 共享session,分布式锁,计数器,限流 |
| List | 列表 | lpush key value / lrange key start end | quicklist | 消息队列,文章列表 |
| Hash | 哈希 | hset key field value / hget key field | ziplist/ht | 缓存用户信息等 |
| Set | 集合 | sadd key element / smembers key | intset/ht | 用户标签,生成随机数抽奖,社交需求 |
| ZSet | 有序集合 | zadd key score member / zrank key member | ziplist/skiplist | 排行榜,社交需求(如用户点赞) |
String(字符串)
String 是 redis 中最基本的数据类型,一个 key 对应一个 value。String 类型是二进制安全的,意思是 redis 的 String 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
内部编码:
字符串对象的编码可以是int,raw或者embstr。
- int 编码:保存的是可以用 long 类型表示的整数值。
- embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
- raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
命令使用:
| 命令 | 简述 | 使用 |
|---|---|---|
| GET | 获取存储在给定键中的值 | GET name |
| SET | 设置存储在给定键中的值 | SET name value |
| DEL | 删除存储在给定键中的值 | DEL name |
| INCR | 将键存储的值加1 | INCR key |
| DECR | 将键存储的值减1 | DECR key |
| INCRBY | 将键存储的值加上整数 | INCRBY key amount |
| DECRBY | 将键存储的值减去整数 | DECRBY key amount |
命令执行:
1 | 127.0.0.1:6379> set hello world |
应用场景:
- 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
- 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
- session:常见方案spring session + redis实现session共享。
List(列表)
Redis 中的 List 其实就是链表(Redis 用双端链表实现 List)。使用 List 结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的 TimeLine)。List 的另一个应用就是消息队列,可以利用 List 的 PUSH 操作,将任务存放在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。
内部编码:
列表对象的编码是quicklist。 (之前版本中有linked和ziplist这两种编码。进一步的, 目前Redis定义的10个对象编码方式宏名中,有两个被完全闲置了,分别是: OBJ_ENCODING_ZIPMAP与OBJ_ENCODING_LINKEDLIST。 从Redis的演进历史上来看, 前者是后续可能会得到支持的编码值(代码还在),后者则应该是被彻底淘汰了)。
命令使用:
| 命令 | 简述 | 使用 |
|---|---|---|
| RPUSH | 将给定值推入到列表右端 | RPUSH key value |
| LPUSH | 将给定值推入到列表左端 | LPUSH key value |
| RPOP | 从列表的右端弹出一个值,并返回被弹出的值 | RPOP key |
| LPOP | 从列表的左端弹出一个值,并返回被弹出的值 | LPOP key |
| LRANGE | 获取列表在给定范围上的所有值 | LRANGE key 0 -1 |
| LINDEX | 通过索引获取列表中的元素。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 | LINDEX key index |
命令执行:
1 | 127.0.0.1:6379> lpush mylist 1 2 ll ls mem |
使用技巧:
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpsh+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
应用场景:
- 微博TimeLine: 有人发布微博,用lpush加入时间轴,展示新的列表信息。
- 消息队列
Hash(哈希)
Redis hash 是一个 String 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
内部编码:
哈希对象的编码可以是ziplist或者hashtable;对应的底层实现有两种,一种是ziplist, 一种是dict。
命令使用:
| 命令 | 简述 | 使用 |
|---|---|---|
| HSET | 添加键值对 | HSET hash-key sub-key1 value1 |
| HGET | 获取指定散列键的值 | HGET hash-key key1 |
| HGETALL | 获取散列中包含的所有键值对 | HGETALL hash-key |
| HDEL | 如果给定键存在于散列中,那么就移除这个键 | HDEL hash-key sub-key1 |
命令执行:
1 | 127.0.0.1:6379> hset user name1 hao |
应用场景:
- 缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
Set(集合)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
内部编码:
集合对象的编码可以是intset或者hashtable;底层实现有两种,分别是intset和dict。 显然当使用intset作为底层实现的数据结构时,集合中存储的只能是数值数据,且必须是整数; 而当使用dict作为集合对象的底层实现时,是将数据全部存储于dict的键中,值字段闲置不用。
命令使用:
| 命令 | 简述 | 使用 |
|---|---|---|
| SADD | 向集合添加一个或多个成员 | SADD key value |
| SCARD | 获取集合的成员数 | SCARD key |
| SMEMBERS | 返回集合中的所有成员 | SMEMBERS key member |
| SISMEMBER | 判断 member 元素是否是集合 key 的成员 | SISMEMBER key member |
命令执行:
1 | 127.0.0.1:6379> sadd myset hao hao1 xiaohao hao |
应用场景:
- 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
- 点赞,或点踩,收藏等,可以放到set中实现。
ZSet(有序集合)
Redis 有序集合和集合一样也是 String 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
内部编码:
有序集合的成员是唯一的,但分数(score)却可以重复。有序集合是通过两种数据结构实现:
- 压缩列表(
ziplist):ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关。 - 跳跃表(
skiplist):跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这是采用跳跃表的主要原因。跳跃表的复杂度是O(log(n))。
命令使用:
| 命令 | 简述 | 使用 |
|---|---|---|
| ZADD | 将一个带有给定分值的成员添加到有序集合里面 | ZADD zset-key 178 member1 |
| ZRANGE | 根据元素在有序集合中所处的位置,从有序集合中获取多个元素 | ZRANGE zset-key 0-1 withccores |
| ZREM | 如果给定元素成员存在于有序集合中,那么就移除这个元素 | ZREM zset-key member1 |
命令执行:
1 | 127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao |
应用场景:
- 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
特殊数据类型
Hyperloglog(基数统计)
Redis 2.8.9 版本更新了 Hyperloglog 数据结构!
什么是基数
举个例子,A = {1, 2, 3, 4, 5}, B = {3, 5, 6, 7, 9};那么基数(不重复的元素)= 1, 2, 4, 6, 7, 9; (允许容错,即可以接受一定误差)
HyperLogLogs 基数统计用来解决什么问题
这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等。它的优势体现在哪?一个大型的网站,每天 IP 比如有 100 万,粗算一个 IP 消耗 15 字节,那么 100 万个 IP 就是 15M。而 HyperLogLog 在 Redis 中每个键占用的内容都是 12K,理论存储近似接近 2^64 个值,不管存储的内容是什么,它一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的基数并不一定准确,是一个带有 0.81% 标准错误的近似值(对于可以接受一定容错的业务场景,比如IP数统计,UV等,是可以忽略不计的)。
相关命令使用
1
2
3
4
5
6
7
8
9
10
11
12127.0.0.1:6379> pfadd key1 a b c d e f g h i # 创建第一组元素
(integer) 1
127.0.0.1:6379> pfcount key1 # 统计元素的基数数量
(integer) 9
127.0.0.1:6379> pfadd key2 c j k l m e g a # 创建第二组元素
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 8
127.0.0.1:6379> pfmerge key3 key1 key2 # 合并两组:key1 key2 -> key3 并集
OK
127.0.0.1:6379> pfcount key3
(integer) 13
Bitmap(位图)
Bitmap 即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。
用来解决什么问题
比如:统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps!如果存储一年的打卡状态需要多少内存呢? 365 天 = 365 bit 1字节 = 8bit 46 个字节左右!
相关命令使用
使用bitmap 来记录 周一到周日的打卡! 周一:1 周二:0 周三:0 周四:1 ……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
查看某一天是否有打卡!
127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 5
(integer) 0
统计操作,统计 打卡的天数!
127.0.0.1:6379> bitcount sign # 统计这周的打卡记录,就可以看到是否有全勤!
(integer) 3
Geospatial(地理空间)
Redis 的 Geo 在 Redis 3.2 版本就推出了! 这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人
geoadd
添加地理位置
1
2
3
4127.0.0.1:6379> geoadd china:city 118.76 32.04 manjing 112.55 37.86 taiyuan 123.43 41.80 shenyang
(integer) 3
127.0.0.1:6379> geoadd china:city 144.05 22.52 shengzhen 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 3规则
两级无法直接添加,我们一般会下载城市数据(这个网址可以查询 GEO)
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
1
2
3当坐标位置超出上述指定范围时,该命令将会返回一个错误。
127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin
(error) ERR invalid longitude,latitude pair 39.900000,116.400000geopos
获取指定的成员的经度和纬度
1
2
3
4
5127.0.0.1:6379> geopos china:city taiyuan manjing
1) 1) "112.54999905824661255"
1) "37.86000073876942196"
2) 1) "118.75999957323074341"
1) "32.03999960287850968"获得当前定位, 一定是一个坐标值!
geodist
如果不存在, 返回空
单位如下
- m
- km
- mi 英里
- ft 英尺
1
2
3
4127.0.0.1:6379> geodist china:city taiyuan shenyang m
"1026439.1070"
127.0.0.1:6379> geodist china:city taiyuan shenyang km
"1026.4391"georadius
附近的人 ==> 获得所有附近的人的地址,定位,通过半径来查询
获得指定数量的人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19127.0.0.1:6379> georadius china:city 110 30 1000 km 以 100,30 这个坐标为中心, 寻找半径为1000km的城市
1) "xian"
2) "hangzhou"
3) "manjing"
4) "taiyuan"
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist
1) 1) "xian"
2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord withdist count 2
1) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
2) 1) "manjing"
2) "864.9816"
3) 1) "118.75999957323074341"
2) "32.03999960287850968"参数 key 经度 纬度 半径 单位 [显示结果的经度和纬度] [显示结果的距离] [显示的结果的数量]
georadiusbymember
显示与指定成员一定半径范围内的其他成员
1
2
3
4
5
6
7
8
9
10
11
12
13127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km
1) "manjing"
2) "taiyuan"
3) "xian"
127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km withcoord withdist count 2
1) 1) "taiyuan"
2) "0.0000"
3) 1) "112.54999905824661255"
2) "37.86000073876942196"
2) 1) "xian"
2) "514.2264"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"参数与 georadius 一样
geohash(较少使用)
该命令返回11个字符的hash字符串
1
2
3127.0.0.1:6379> geohash china:city taiyuan shenyang
1) "ww8p3hhqmp0"
2) "wxrvb9qyxk0"将二维的经纬度转换为一维的字符串, 如果两个字符串越接近, 则距离越近
过期策略
Redis中同时使用了惰性过期和定期过期两种过期策略
假设Redis当前存放30万个key,并且都设置了过期时间,如果每隔100ms就去检查这全部的key,CPU负载会特别高,最后可能会挂掉。因此,redis采取的是定期过期,每隔100ms就随机抽取一定数量的key来检查和删除的。但是,最后可能会有很多已经过期的key没被删除。这时候,redis采用惰性删除。在获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。但是,如果定期删除漏掉了很多过期的key,然后也没走惰性删除。就会有很多过期key积在内存内存,直接会导致内存爆满。或者有些时候,业务量大起来了,redis的key被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。难道redis直接这样挂掉?不会的!Redis用8种内存淘汰策略保护自己。
定时过期
该策略可以立即清除过期的数据
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除,对内存很友好,但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除
该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期
隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
内存淘汰策略
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据
noeviction
默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
volatile-lru
当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰。
allkeys-lru
当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
volatile-lfu
4.0版本新增,当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LFU(最少频率使用)算法进行淘汰。
allkeys-lfu
4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU(最少频率使用)算法进行淘汰。
volatile-random
当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据。
allkeys-random
当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
volatile-ttl
当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰。
LRU与LFU
LRU(Least Recently Used,最近最少使用)
LRU算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。基本思路:新数据插入到列表头部;每当缓存命中(即缓存数据被访问),则将数据移到列表头部;当列表满的时候,将列表尾部的数据丢弃。
LFU(Least Frequently Used,最少频率使用)
它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路。LFU需要定期衰减。
持久化机制
Redis允许我们同时使用两种机制,即使AOF和RDB都开启了,redis重启的时候,也是优先通过AOF进行数据恢复的,因为aof数据比较完整。
通常情况下我们会设置AOF机制为everysec 每秒写入,则最坏仅会丢失一秒内的数据。
注:如果先开启了RDB,再开启AOF,RDB先执行了持久化,那么RDB文件中的内容会被AOF覆盖掉。

RDB
RDB(Redis DataBase),就是把内存数据以快照的形式保存到磁盘上。
在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis重启的时候,通过加载dump.rdb文件来恢复数据。
触发机制
手动触发(一般不会用)
save:同步,阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用。
bgsave:异步,Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
具体流程如下:
- redis客户端执行bgsave命令或者自动触发bgsave命令;
- 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
- 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
- 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;
- 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)。
自动触发
- redis.conf中配置
save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件; - 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;
- 执行debug reload命令重新加载redis时也会触发bgsave操作;
- 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作。
- redis.conf中配置
RDB配置
内存快照虽然可以通过技术人员手动执行SAVE或BGSAVE命令来进行,但生产环境下多数情况都会设置其周期性执行条件。
1 | # 周期性执行条件的设置格式为 |
-
save 900 1:如果900秒内有1条Key信息发生变化,则进行快照; -
save 300 10:如果300秒内有10条Key信息发生变化,则进行快照; -
save 60 10000:如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整。 -
dbfilename:RDB文件在磁盘上的名称。 -
dir:RDB文件的存储路径。默认设置为“./”,也就是Redis服务的主目录。 -
stop-writes-on-bgsave-error:上文提到的在快照进行过程中,主进程照样可以接受客户端的任何写操作的特性,是指在快照操作正常的情况下。如果快照操作出现异常(例如操作系统用户权限不够、磁盘空间写满等等)时,Redis就会禁止写操作。这个特性的主要目的是使运维人员在第一时间就发现Redis的运行错误,并进行解决。一些特定的场景下,您可能需要对这个特性进行配置,这时就可以调整这个参数项。该参数项默认情况下值为yes,如果要关闭这个特性,指定即使出现快照错误Redis一样允许写操作,则可以将该值更改为no。 -
rdbcompression:该属性将在字符串类型的数据被快照到磁盘文件时,启用LZF压缩算法。Redis官方的建议是请保持该选项设置为yes,因为“it’s almost always a win”。 -
rdbchecksum:从RDB快照功能的version 5版本开始,一个64位的CRC冗余校验编码会被放置在RDB文件的末尾,以便对整个RDB文件的完整性进行验证。这个功能大概会多损失10%左右的性能,但获得了更高的数据可靠性。所以如果您的Redis服务需要追求极致的性能,就可以将这个选项设置为no。
RDB的优点
- RDB在恢复大数据集时,速度比AOF的恢复速度要快。适合大规模的数据恢复场景,如备份,全量复制等。
- RDB可以最大化Redis的性能。因为父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无需执行任何磁盘I/O操作。但是如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求。
RDB的缺点
- RDB方式实时性不够,无法做到秒级的持久化。
- 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高。
- RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全。
- 版本兼容RDB文件问题。
Copy On Write
问题:由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢?
RDB 过程中主进程会 fork 出一个子进程,子进程做数据备份操作,主进程继续对外提供服务,所以 Redis 服务不会阻塞;只有在主进程发生写操作修改内存数据时,才会真正去分配内存空间,并复制内存数据,而且也只是复制被修改的内存页中的数据,并不是全部内存数据。

- Redis中执行BGSAVE命令生成RDB文件时,本质就是调用Linux中的fork()命令,Linux下的fork()系统调用实现了Copy-on-Write写时复制。
- fork()是类Unix操作系统上创建线程的主要方法,fork用于创建子进程(等同于当前进程的副本)。
- 传统的普通进程复制,会直接将父进程的数据拷贝到子进程中,拷贝完成后,主进程和子进程之间的数据段和堆栈是相互独立的。
- Copy-on-Write技术,在fork出子进程后,与主进程共享内存空间,两者只是虚拟空间不同,但是其对应的物理空间是同一个。
AOF
AOF(Append Only File)持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。
它主要解决数据持久化的实时性问题,默认是不开启的。
实现机制
AOF日志记录Redis的每个写命令,步骤分为:命令追加(append),文件写入(write)和文件同步(sync)。
命令追加
当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
文件写入和同步
关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:
配置项 写回时机 优点 缺点 Always 同步写回 可靠性高,数据基本不丢失 每个写命令都要落盘,性能影响较大 Everysec 每秒写回 性能适中 宕机时丢失1秒内的数据 No 操作系统控制的写回 性能好 宕机时丢失数据较多 -
Always:同步写回:每个写命令执行完,立马同步地将日志写回磁盘。 -
Everysec:每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘。 -
No:操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
-
AOF配置
默认情况下,Redis是没有开启AOF的,可以通过配置redis.conf文件来开启AOF持久化。
1 | # appendonly参数开启AOF持久化 |
appendonly:默认情况下AOF功能是关闭的,将该选项改为yes以便打开Redis的AOF功能。
appendfilename:这个参数项很好理解了,就是AOF文件的名字。
appendfsync:这个参数项是AOF功能最重要的设置项之一,主要用于设置“真正执行”操作命令向AOF文件中同步的策略。什么叫“真正执行”呢?还记得Linux操作系统对磁盘设备的操作方式吗? 为了保证操作系统中I/O队列的操作效率,应用程序提交的I/O操作请求一般是被放置在linux Page Cache中的,然后再由Linux操作系统中的策略自行决定正在写到磁盘上的时机。而Redis中有一个fsync()函数,可以将Page Cache中待写的数据真正写入到物理设备上,而缺点是频繁调用这个fsync()函数干预操作系统的既定策略,可能导致I/O卡顿的现象频繁 。与上节对应,appendfsync参数项可以设置三个值,分别是:always、everysec、no,默认的值为everysec。
no-appendfsync-on-rewrite:always和everysec的设置会使真正的I/O操作高频度的出现,甚至会出现长时间的卡顿情况,这个问题出现在操作系统层面上,所有靠工作在操作系统之上的Redis是没法解决的。为了尽量缓解这个情况,Redis提供了这个设置项,保证在完成fsync函数调用时,不会将这段时间内发生的命令操作放入操作系统的Page Cache(这段时间Redis还在接受客户端的各种写操作命令)。
auto-aof-rewrite-percentage:上文说到在生产环境下,技术人员不可能随时随地使用“BGREWRITEAOF”命令去重写AOF文件。所以更多时候我们需要依靠Redis中对AOF文件的自动重写策略。Redis中对触发自动重写AOF文件的操作提供了两个设置:auto-aof-rewrite-percentage表示如果当前AOF文件的大小超过了上次重写后AOF文件的百分之多少后,就再次开始重写AOF文件。例如该参数值的默认设置值为100,意思就是如果AOF文件的大小超过上次AOF文件重写后的1倍,就启动重写操作。
auto-aof-rewrite-min-size:参考auto-aof-rewrite-percentage选项的介绍,auto-aof-rewrite-min-size设置项表示启动AOF文件重写操作的AOF文件最小大小。如果AOF文件大小低于这个值,则不会触发重写操作。注意,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size只是用来控制Redis中自动对AOF文件进行重写的情况,如果是技术人员手动调用“BGREWRITEAOF”命令,则不受这两个限制条件左右。
AOF的优点
- 数据的一致性和完整性更高。
AOF的缺点
- 记录的内容越多,文件越大,数据恢复变慢。
高可用
主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
主从复制作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
主从库之间采用的是读写分离的方式:
- 读操作:主库、从库都可以接收。
- 写操作:首先到主库执行,然后,主库将写操作同步给从库。
主从复制原理
注意:在2.8版本之前只有全量复制,而2.8版本后有全量和增量复制。
全量(同步)复制
第一次同步时。
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。
确立主从关系
例如,现在有实例 1(ip:172.16.19.3)和实例 2(ip:172.16.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:
1
replicaof 172.16.19.3 6379
全量复制的三个阶段

第一阶段:是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。
第二阶段:主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
第三阶段:主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
增量(同步)复制
只会把主从库网络断连期间主库收到的命令,同步给从库。
为什么会设计增量复制
如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。
增量复制的流程

repl_backlog_buffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。而在repl_backlog_buffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。
replication buffer:Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传如果在网络断开期间,repl_backlog_size环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?对于这个问题来说,有两个关键点:
- 一个从库如果和主库断连时间过长,造成它在主库repl_backlog_buffer的slave_repl_offset位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
- 每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。# 更深入理解
哨兵机制(Redis Sentinel)
主从复制模式下,由哨兵来监控选举主节点
Redis Sentinel provides high availability for Redis when not using Redis Cluster.
Sentinel作用
- 监控(Monitoring) :Sentinel会不断地检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification) :当被监控的某个Redis服务器出现问题时,Sentinel可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automatic failover) :当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
Sentinel启动
对于 redis-sentinel 程序, 你可以用以下命令来启动 Sentinel 系统:
1 | redis-sentinel /path/to/sentinel.conf |
对于 redis-server 程序, 你可以用以下命令来启动一个运行在 Sentinel 模式下的 Redis 服务器:
1 | redis-server /path/to/sentinel.conf --sentinel |
Sentinel下线
- 主观下线(Subjectively Down, 简称 SDOWN) :指的是单个 Sentinel 实例对服务器做出的下线判断。
- 客观下线(Objectively Down, 简称 ODOWN) :指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)
Sentinel工作流程
- 每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线。 一个有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
- 如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。
- 如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
- 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主观下线状态就会被移除。
分片技术(Redis Cluster)

Redis集群介绍
Redis集群是一个提供在多个Redis间节点间共享数据的程序集。
Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。
Redis集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。
Redis集群的优势:
- 自动分割数据到不同的节点上。
- 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。
Redis集群的数据分片
Redis集群没有使用一致性hash,而是引入了 哈希槽的概念。
Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽。
举个例子,比如当前集群有3个节点,那么:
- 节点 A 包含 0 到 5500号哈希槽。
- 节点 B 包含 5501 到 11000 号哈希槽。
- 节点 C 包含 11001 到 16384 号哈希槽。
这种结构很容易添加或者删除节点。比如如果我想新添加个节点D,我需要从节点 A, B, C中得部分槽到D上。如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
Redis集群的主从复制模型
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品。
在我们例子中具有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。
然而如果在集群创建的时候(或者过一段时间)我们为每个节点添加一个从节点A1,B1,C1,那么整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,整个集群便不会因为槽找不到而不可用了。不过当B和B1都失败后,集群是不可用的。
Redis一致性保证
Redis并不能保证数据的强一致性。这意味这在实际中集群在特定的条件下可能会丢失写操作。
第一个原因是因为集群是用了异步复制。写操作过程:
- 客户端向主节点B写入一条命令。
- 主节点B向客户端回复命令状态。
- 主节点将写操作复制给他得从节点 B1,B2 和 B3。
主节点对命令的复制工作发生在返回命令回复之后,因为如果每次处理命令请求都需要等待复制操作完成的话,那么主节点处理命令请求的速度将极大地降低(我们必须在性能和一致性之间做出权衡)。
注意:Redis 集群可能会在将来提供同步写的方法。
Redis集群另外一种可能会丢失命令的情况是集群出现了网络分区,并且一个客户端与至少包括一个主节点在内的少数实例被孤立。
举个例子:假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1 假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1 ,小部分的一方则包含节点 B 和客户端 Z1 。Z1仍然能够向主节点B中写入,如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了。
注意:在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的,这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。
分区实现
分区:数据分布到多个redis实例
Redis分区主要有两个目的:
- 分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。
- 分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。
Redis集群
同上Redis Cluster
Twemproxy
Twemproxy是Twitter维护的(缓存)代理系统,代理Memcached的ASCII协议和Redis协议。它是单线程程序,使用c语言编写,运行起来非常快。它是采用Apache 2.0 license的开源软件。
Twemproxy支持自动分区,如果其代理的其中一个Redis节点不可用时,会自动将该节点排除(这将改变原来的keys-instances的映射关系,所以你应该仅在把Redis当缓存时使用Twemproxy)。
Twemproxy本身不存在单点问题,因为你可以启动多个Twemproxy实例,然后让你的客户端去连接任意一个Twemproxy实例。
Twemproxy是Redis客户端和服务器端的一个中间层,由它来处理分区功能应该不算复杂,并且应该算比较可靠的。
一致性哈希

面试要点
Redis为什么这么快
基于内存存储实现
Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
高效的数据结构
动态字符串、链表、压缩列表,跳跃表。
合理的数据编码
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果
合理的线程模型
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

虚拟内存机制
Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
基于Redis的分布式锁如何实现
- 使用setNx命令
- 设置一个过期时间
- 另起一个线程监控锁,超时续期

参考实现代码:

缓存击穿,缓存穿透与缓存雪崩

缓存击穿
指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。
解决方案:
- 使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
- “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
缓存穿透
读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。
解决方案:
- 如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
- 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如果有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
- 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
缓存雪崩
指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。
解决方案:
- 缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒。
- Redis故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。
以上文章摘自20道经典Redis面试题,Redis过期策略及持久化机制