Redis:高性能的内存数据库

发表时间: 2022-09-24 15:31



Redis

概念和基础

Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。

什么是Redis

Redis是一款内存高速缓存数据库。Redis全称为:Remote Dictionary Server(远程数据服务),使用C语言编写,Redis是一个key-value存储系统(键值存储系统),支持丰富的数据类型,如:String、list、set、zset、hash。

Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。

为什么用Redis

读写性能优异 数据类型丰富 原子性 丰富的特性 持久化 发布订阅 分布式

哪些场景用Redis

热点数据的缓存

缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。

作为缓存使用时,一般有两种方式保存数据:

读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis。(实施起来简单)

插入数据时,同时写入Redis。(数据实时性强,但是开发时不便于统一处理。)

如何保证redis缓存与数据一致性问题?

以用户余额为例:

当用户的余额发生变化的时候,如何更新缓存中的数据

1.我是先更新缓存中的数据再更新数据库的数据;

2.还是先修改数据库中的数据再更新缓存中的数据

数据库的数据和缓存中的数据如何达到一致性?

首先,可以肯定的是,redis中的数据和数据库中的数据不可能保证事务性达到统一的

所以在实际应用中,我们都是基于当前的场景进行权衡降低出现不一致问题的出现概率。

方案一:保证最终一致性

可以通过给缓存设置一个过期时间,无论以上的两种操作使用哪一种,都能够在理论上保持数据的最终一致性。

这种方案下,在写数据的时候,都以数据库为主,先把数据库数据写入后,再更新缓存,如果失败了,那么原来的缓存无论是否存在,那么只要经过过期时间,那么读操作时,就会重新从数据库中读出,然后同步到缓存中。

方案二:先删除缓存再更新数据库(不推荐)

这种处理方式,可能会有如下情况,线程A,写入(更新)数据时,先删除缓存后,同时,有一个线程B,此时去读数据,先去缓存中读,此时刚被线程A删除了,于是去数据库读还未被线程A更新的数据,这时候就会读到脏数据了。 紧接着,线程B会把脏数据写入缓存,而线程A会把更新后的数据写入数据库。导致缓存与数据不一致。

参考方式: 给缓存设置过期时间,否则会一直读到脏数据。

解决方案:双删策略

1.先删除缓存

2.写入数据库

3.休眠一秒。执行删除缓存(目的是把1秒内产生的脏数据重新从缓存中删除)

缺点: 需要把控好休眠时间,同时时间可能会过长,当请求量很大时,再短的时间也会造成响应过长。其次这个请求时间是跟读操作时,会产生脏数据的时间有关的。

为什么要用延时双删:不能数据库更新后再删除吗?

如果采用读写分离数据库的情况呢? 跟以上情况的区别就在于,当线程B去读数据库时,可能读到的是还未更新到从库的脏数据,因为当我们休眠是,不仅需要考虑读操作的完整执行完的时间,还需要加主从复制这段时间的几百Ms。 方案三:先更新数据库再更新缓存中的数据

限时业务的运用

redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。

计数器相关问题

redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。

分布式锁

这个主要利用redis的setnx命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在俞你奔远方的后台中有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock, 如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。 当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。 在分布式锁的场景中,主要用在比如秒杀系统等。

正确使用redis分布式锁的范式

其实Redis分布式锁比较正确的姿势是采用redisson这个客户端工具,只要客户端一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断延长锁key的生存时间。 默认情况下,加锁的时间是30秒,.如果加锁的业务没有执行完,就会进行一次续期,把锁重置成30秒.那这个时候可能又有同学问了,那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了呗.

延时操作

比如在订单生产后我们占用了库存,10分钟后去检验用户是够真正购买,如果没有购买将该单据设置无效,同时还原库存。 由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。 所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。 当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。

排行榜相关问题

关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。 比如点赞排行榜,做一个SortedSet, 然后以用户的openid作为上面的username, 以用户的点赞数作为上面的score, 然后针对每个用户做一个hash, 通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。

点赞、好友等相互关系的存储

Redis 利用集合的一些命令,比如求交集、并集、差集等。

在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。

签到问题

1: https://juejin.cn/post/6881928046031568903

2: https://juejin.cn/post/6882657262762983438

3: https://juejin.cn/post/6904138145923596295

数据结构

这些适用场景都是基于Redis支持的数据类型的,所以我们需要学习它支持的数据类型;同时在redis优化中还需要对底层数据结构了解,所以也需要了解一些底层数据结构的设计和实现。

5种基础数据类型

首先对redis来说,所有的key(键)都是字符串。我们在谈基础数据结构时,讨论的是存储值的数据类型,主要包括常见的5种数据类型,分别是:String、List、Set、Zset、Hash。

String

String是redis中最基本的数据类型,一个key对应一个value。

String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象

命令

GET

获取存储在给定键中的值

SET

设置存储在给定键中的值

DEL

删除存储在给定键中的值

INCR

将键存储的值加1

DECR

将键存储的值减1

INCRBY

将键存储的值加上整数

DECRBY

将键存储的值减去整数

使用场景

缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。

计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。

session:常见方案spring session + redis实现session共享

List

使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。

命令

RPUSH

将给定值推入到列表右端

LPUSH

将给定值推入到列表左端

RPOP

从列表的右端弹出一个值,并返回被弹出的值

LPOP

从列表的左端弹出一个值,并返回被弹出的值

LRANGE

获取列表在给定范围上的所有值

LINDEX

通过索引获取列表中的元素。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

使用场景

缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。

计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。

session:常见方案spring session + redis实现session共享

Hash

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象

使用场景

缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等

命令

HSET

添加键值对

HGET

获取指定散列键的值

HGETALL

获取散列中包含的所有键值对

HDEL

如果给定键存在于散列中,那么就移除这个键

Set

Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据

命令

SADD

向集合添加一个或多个成员

SCARD

获取集合的成员数

SMEMBER

返回集合中的所有成员

SISMEMBER

判断 member 元素是否是集合 key 的成员

使用场景

标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人

点赞,或点踩,收藏等,可以放到set中实现

ZSet

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。

命令

ZADD

将一个带有给定分值的成员添加到哦有序集合里面

ZRANGE

根据元素在有序集合中所处的位置,从有序集合中获取多个元素

ZREM

如果给定元素成员存在于有序集合中,那么就移除这个元素

使用场景

排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。

3种特殊类型

HyperLogLog(基数统计)

什么是基数? HyperLogLogs 基数统计用来解决什么问题? 它的优势体现在哪? 相关命令使用

Bitmap(位存储)

用来解决什么问题?

比如:统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps! 如果存储一年的打卡状态需要多少内存呢? 365 天 = 365 bit 1字节 = 8bit 46 个字节左右!

相关命令使用

使用bitmap 来记录 周一到周日的打卡! 周一:1 周二:0 周三:0 周四:1 ……

127.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

Geo(地理位置)

geoadd geopos

Stream类型

为什么会设计Stream

Stream详解

Stream的结构 增删改查 独立消费 消费组消费 信息监控

更深入理解

Stream用在什么样场景 消息ID的设计是否考虑了时间回拨的问题? 消费者崩溃带来的会不会消息丢失问题? 消费者彻底宕机后如何转移给其它消费者处理? 坏消息问题,Dead Letter,死信问题

对象机制

redisObject 对象共享 对象淘汰

底层数据结构

 底层数据结构引入

简单动态字符串 - sds

Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字符’Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。

这是一种用于存储二进制数据的一种结构, 具有动态扩容的特点. 其实现位于src/sds.h与src/sds.c中。

结构: 

为什么使用SDS?

常数复杂度获取字符串长度

由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。

杜绝缓冲区溢出

我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

减少修改字符串的内存重新分配次数

C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。

而对于SDS,由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:

1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

2、惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 alloc 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)

二进制安全

因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。

兼容部分 C 字符串函数

虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库 中的一部分函数

压缩列表 - ZipList

ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。他能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关。

ziplist结构

zlbytes字段的类型是uint32_t, 这个字段中存储的是整个ziplist所占用的内存的字节数

zltail字段的类型是uint32_t, 它指的是ziplist中最后一个entry的偏移量. 用于快速定位最后一个entry, 以快速完成pop等操作

zllen字段的类型是uint16_t, 它指的是整个ziplit中entry的数量. 这个值只占2bytes(16位): 如果ziplist中entry的数目小于65535(2的16次方), 那么该字段中存储的就是实际entry的值. 若等于或超过65535, 那么该字段的值固定为65535, 但实际数量需要一个个entry的去遍历所有entry才能得到.

zlend是一个终止字节, 其值为全F, 即0xff. ziplist保证任何情况下, 一个entry的首字节都不会是255 Entry结构

为什么ZipList特别省内存

ziplist节省内存是相对于普通的list来说的,如果是普通的数组,那么它每个元素占用的内存是一样的且取决于最大的那个元素(很明显它是需要预留空间的);

所以ziplist在设计时就很容易想到要尽量让每个元素按照实际的内容大小存储,所以增加encoding字段,针对不同的encoding来细化存储大小;

这时候还需要解决的一个问题是遍历元素时如何定位下一个元素呢?在普通数组中每个元素定长,所以不需要考虑这个问题;但是ziplist中每个data占据的内存不一样,所以为了解决遍历,需要增加记录上一个元素的length,所以增加了prelen字段。

ziplist的缺点

ziplist也不预留内存空间, 并且在移除结点后, 也是立即缩容, 这代表每次写操作都会进行内存分配操作. 结点如果扩容, 导致结点占用的内存增长, 并且超过254字节的话, 可能会导致链式反应: 其后一个结点的entry.prevlen需要从一字节扩容至五字节. 最坏情况下, 第一个结点的扩容, 会导致整个ziplist表中的后续所有结点的entry.prevlen字段扩容. 虽然这个内存重分配的操作依然只会发生一次, 但代码中的时间复杂度是o(N)级别, 因为链式扩容只能一步一步的计算. 但这种情况的概率十分的小, 一般情况下链式扩容能连锁反映五六次就很不幸了. 之所以说这是一个蛋疼问题, 是因为, 这样的坏场景下, 其实时间复杂度并不高: 依次计算每个entry新的空间占用, 也就是o(N), 总体占用计算出来后, 只执行一次内存重分配, 与对应的memmove操作, 就可以了

快表 - QuickList

quicklist结构

它是一种以ziplist为结点的双端链表结构. 宏观上, quicklist是一个链表, 微观上, 链表中的每个结点都是一个ziplist。

quicklist内存布局图

quicklist更多额外信息

quicklist有自己的优点, 也有缺点, 对于使用者来说, 其使用体验类似于线性数据结构, list作为最传统的双链表, 结点通过指针持有数据, 指针字段会耗费大量内存. ziplist解决了耗费内存这个问题. 但引入了新的问题: 每次写操作整个ziplist的内存都需要重分配. quicklist在两者之间做了一个平衡. 并且使用者可以通过自定义quicklist.fill, 根据实际业务情况, 经验主义调参

quicklist.fill的值影响着每个链表结点中, ziplist的长度. 当数值为负数时, 代表以字节数限制单个ziplist的最大长度. 具体为: -1 不超过4kb -2 不超过 8kb -3 不超过 16kb -4 不超过 32kb -5 不超过 64kb 当数值为正数时, 代表以entry数目限制单个ziplist的长度. 值即为数目. 由于该字段仅占16位, 所以以entry数目限制ziplist的容量时, 最大值为2^15个 quicklist.compress的值影响着quicklistNode.zl字段指向的是原生的ziplist, 还是经过压缩包装后的quicklistLZF 0 表示不压缩, zl字段直接指向ziplist 1 表示quicklist的链表头尾结点不压缩, 其余结点的zl字段指向的是经过压缩后的quicklistLZF 2 表示quicklist的链表头两个, 与末两个结点不压缩, 其余结点的zl字段指向的是经过压缩后的quicklistLZF 4.以此类推, 最大值为2^16

quicklistNode.encoding字段, 以指示本链表结点所持有的ziplist是否经过了压缩. 1代表未压缩, 持有的是原生的ziplist, 2代表压缩过 quicklistNode.container字段指示的是每个链表结点所持有的数据类型是什么. 默认的实现是ziplist, 对应的该字段的值是2,目前Redis没有提供其它实现. 所以实际上, 该字段的值恒为2 quicklistNode.recompress字段指示的是当前结点所持有的ziplist是否经过了解压. 如果该字段为1即代表之前被解压过, 且需要在下一次操作时重新压缩. quicklist的具体实现代码篇幅很长, 这里就不贴代码片断了, 从内存布局上也能看出来, 由于每个结点持有的ziplist是有上限长度的, 所以在与操作时要考虑的分支情况比较多。

字典/哈希表 - Dict 整数集 - IntSet

跳表 - ZSkipList

跳跃表结构在 Redis 中的运用场景只有一个,那就是作为有序列表 (Zset) 的使用。跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这就是跳跃表的长处。跳跃表的缺点就是需要的存储空间比较大,属于利用空间来换取时间的数据结构

核心知识

持久化

RDB 与 AOF都有其自己所适合的场景

RDB适合全量复制,而AOF适合做增量复制的场景

Redis持久化简介

为什么需要持久化?

Redis是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。通常的解决方案是从后端数据库恢复这些数据,但后端数据库有性能瓶颈,如果是大数据量的恢复, 1、会对数据库带来巨大的压力, 2、数据库的性能不如Redis。导致程序响应慢。所以对Redis来说,实现数据的持久化,避免从后端数据库中恢复数据,是至关重要的。

Redis持久化有哪些方式呢?

为什么我们需要重点学RDB和AOF? 从严格意义上说,Redis服务提供四种持久化存储方案:RDB、AOF、虚拟内存(VM)和 DISKSTORE

RDB持久化

RDB 就是 Redis DataBase 的缩写,中文名为快照/内存快照,RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。

触发方式

手动触发

save命令

bgsave命令

自动触发

1.redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;

2.主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点

3.执行debug reload命令重新加载redis时也会触发bgsave操作;

4.默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作

配置RDB

快照周期

内存快照虽然可以通过技术人员手动执行SAVE或BGSAVE命令来进行,但生产环境下多数情况都会设置其周期性执行条件。

周期性执行条件的设置格式为

save

默认的设置为:

save 900 1 save 300 10 save 60 10000

以下设置方式为关闭RDB快照功能

save ""

以上三项默认信息设置代表的意义是:

如果900秒内有1条Key信息发生变化,则进行快照;

如果300秒内有10条Key信息发生变化,则进行快照;

如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整。

其他配置

文件名称

dbfilename dump.rdb

文件保存路径

dir /home/work/app/redis/data/

如果持久化出错,主进程是否停止写入

stop-writes-on-bgsave-error yes

是否压缩

rdbcompression yes

导入时是否检查

rdbchecksum yes

深入理解RDB

AOF持久化

aof以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

Redis是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。PS: 大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。

如何实现AOF redis.conf中配置AOF 深入理解AOF重写 AOF和RDB混合方式(4.0)

消息传递

Redis发布订阅简介

发布/订阅使用

基于频道(Channel)的发布/订阅 基于模式(pattern)的发布/订阅

深入理解

基于频道(Channel)的发布/订阅如何实现 基于模式(Pattern)的发布/订阅如何实现的? SpringBoot结合Redis发布/订阅实例?

事件

事件机制

文件事件

1. 为什么单线程的 Redis 能那么快?

因为redis是基于内存存储的

基于多路复用IO模型的Reactor模式

  1. Redis事件响应框架ae_event及文件事件处理器
  2. Redis IO多路复用模型

时间事件

定时事件 周期性事件

aeEventLoop的具体实现

创建事件管理器 创建文件事件 事件处理

事务

什么是Redis事务

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务相关命令和使用

MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。

EXEC:执行事务中的所有操作命令。 DISCARD:取消事务,放弃执行事务块中的所有命令。

WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。

UNWATCH:取消WATCH对所有key的监视

标准的事务执行

给k1、k2分别赋值,在事务中修改k1、k2,执行事务后,查看k1、k2值都被修改。

127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 11 QUEUED 127.0.0.1:6379> set k2 22 QUEUED 127.0.0.1:6379> EXEC

1) OK 2) OK 127.0.0.1:6379> get k1 "11" 127.0.0.1:6379> get k2 "22" 127.0.0.1:6379>

事务取消

127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 33 QUEUED 127.0.0.1:6379> set k2 34 QUEUED 127.0.0.1:6379> DISCARD OK

事务出现错误的处理

语法错误

在开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败,k1、k2保留原值。

127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 11 QUEUED 127.0.0.1:6379> sets k2 22 (error) ERR unknown command sets, with args beginning with: k2, 22, 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get k1 "v1" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379>

Redis类型错误

在开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值

127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k1 v2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 11 QUEUED 127.0.0.1:6379> lpush k2 22 QUEUED 127.0.0.1:6379> EXEC

1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> get k1 "11" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379>

CAS操作实现乐观锁

Redis事务执行步骤

为什么 Redis 不支持回滚? 如何理解Redis与事务的ACID? Redis事务其它实现 更深入的理解

高可用

主从复制概述

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

主要作用是:

数据冗余

主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

故障恢复

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

负载均衡

在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

高可用基石

除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。 主从库之间采用的是读写分离的方式。

主从复制原理

注意:在2.8版本之前只有全量复制,而2.8版本后有全量和增量复制:

全量(同步)复制:比如第一次同步时 增量(同步)复制:只会把主从库网络断连期间主库收到的命令,同步给从库

全量复制

确立主从关系

例如,现在有实例 1(ip:172.16.19.3)和实例 2(ip:172.16.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 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 开始,网络断了之后,主从库会采用增量复制的方式继续同步

增量复制流程

replbacklogbuffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。 如果从库断开时间太久,replbacklogbuffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以replbacklogbuffer配置尽量大一些,可以降低主从断开后全量复制的概率。 而在replbacklogbuffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。

replication buffer:Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。

更深入理解

当主服务器不进行持久化时复制的安全性 为什么主从全量复制使用RDB而不使用AOF? 为什么还有无磁盘复制模式? 为什么还会有从库的从库的设计? 读写分离及其中的问题

高可扩展

哨兵机制

Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移 

哨兵实现了什么功能呢?

监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。

自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。

配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。

通知(Notification):哨兵可以将故障转移的结果发送给客户端。 哨兵集群的组建 哨兵监控Redis库 主库下线的判定 哨兵集群的选举 新主库的选出 故障的转移

分片基数

Redis集群的设计目标

Redis Cluster goals Clients and Servers roles in the Redis Cluster protocol Write safety 可用性(Availability) 性能(Performance) 避免合并(merge)操作 主要模块介绍 请求重定向 状态检测及维护 故障恢复(Failover) 扩容&缩容 更深入理解 其它常见方案

应用实践

缓存问题

缓存穿透

缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。

通过布隆器来解决

缓存击穿

缓存击穿,是指一个key非常热点,被经常访问,当时在一个节点突然失效了。 续的大并发就穿破缓存,直接请求数据库

设置缓存永不过期

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效。

在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

 缓存污染(或满了) 数据库和缓存一致性

性能调优

Redis真的变慢了吗? 使用复杂度过高的命令 操作bigkey 集中过期 实例内存达到上限 fork耗时严重 开启内存大页 开启AOF 绑定CPU 使用Swap 碎片整理 网络带宽过载 其他原因

新版本特性

Redis 4

模块系统 PSYNC 2.0 缓存驱逐策略优化

Lazy Free

在 Redis 4.0 之前, 用户在使用 DEL命令删除体积较大的键, 又或者在使用 FLUSHDB 和 FLUSHALL删除包含大量键的数据库时, 都可能会造成服务器阻塞。

为了解决以上问题, Redis 4.0 新添加了UNLINK命令, 这个命令是DEL命令的异步版本, 它可以将删除指定键的操作放在后台线程里面执行, 从而尽可能地避免服务器阻塞:

redis> UNLINK fruits (integer) 1

因为一些历史原因, 执行同步删除操作的DEL命令将会继续保留。此外, Redis 4.0 中的FLUSHDB和FLUSHALL这两个命令都新添加了ASYNC选项, 带有这个选项的数据库删除操作将在后台线程进行:

redis> FLUSHDB ASYNC OK

redis> FLUSHALL ASYNC OK

还有,执行rename oldkey newkey时,如果newkey已经存在,Redis会先删除已经存在的newkey,这也会引发上面提到的删除大key问题。如果想让Redis在这种场景下也使用lazyfree的方式来删除,可以按如下配置:

lazyfree-lazy-server-del yes

交换数据库 混合持久化 内存命令 兼容NAT和Docker 其他

Redis 5

Stream类型 新的Redis模块API 集群管理器更改 Lua改进 RDB格式变化 动态HZ ZPOPMIN&ZPOPMAX命令 CLIENT新增命令

Redis 6

多线程IO

Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。

所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

Redis6.0的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf配置文件:io-threads-do-reads yes。开启多线程后,还需要设置线程数,否则是不生效的。修改redis.conf配置文件:io-threads ,关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。

SSL支持

连接支持SSL协议,更加安全。

SSL(Secure Sockets Layer 安全套接层协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层与应用层之间对网络连接进行加密。

ACL支持

在之前的版本中,Redis都会有这样的问题:用户执行FLUSHAL,现在整个数据库就空了在以前解决这个问题的办法可能是在Redis配置中将危险命令进行rename,这样将命令更名为随机字符串或者直接屏蔽掉,以满足需要。当有了ACL之后,你就可以控制比如:这个连接只允许使用RPOP,LPUSH这些命令,其他命令都无法调用。

Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。它的工作方式是:在客户端连接之后,需要客户端进行身份验证,以提供用户名和有效密码:如果身份验证阶段成功,则将连接与指定用户关联,并且该用户具有指定的限制。可以对Redis进行配置,使新连接通过“默认”用户进行身份验证(这是默认配置),但是只能提供特定的功能子集

在默认配置中,Redis 6(第一个具有ACL的版本)的工作方式与Redis的旧版本完全相同,也就是说,每个新连接都能够调用每个可能的命令并访问每个键,因此ACL功能对于客户端和应用程序与旧版本向后兼容。同样,使用requirepass配置指令配置密码的旧方法仍然可以按预期工作,但是现在它的作用只是为默认用户设置密码。

RESP3

RESP(Redis Serialization Protocol)是 Redis 服务端与客户端之间通信的协议。Redis 6之前使用的是 RESP2,而Redis 6开始在兼容RESP2的基础上,开始支持RESP3。在Redis 6中我们可以使用HELLO命令在RESP2和RESP3协议之间进行切换

推出RESP3的目的:一是因为希望能为客户端提供更多的语义化响应(semantical replies),降低客户端的复杂性,以开发使用旧协议难以实现的功能;另一个原因是为了实现 Client side caching(客户端缓存)功能。详细见:
https://github.com/antirez/RESP3/blob/master/spec.md

客户端缓存

基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。不过该特性目前合并到了unstable 分支,作者说等6.0 GA版本之前,还要修改很多。 客户端缓存的功能是该版本的全新特性,服务端能够支持让客户端缓存values,Redis作为一个本身作为一个缓存数据库,自身的性能是非常出色的,但是如果可以在Redis客户端再增加一层缓存结果,那么性能会更加的出色。Redis实现的是一个服务端协助的客户端缓存,叫做tracking。 当tracking开启时, Redis会"记住"每个客户端请求的key,当key的值发现变化时会发送失效信息给客户端。失效信息可以通过 RESP3 协议发送给请求的客户端,或者转发给一个不同的连接(支持RESP2+ Pub/Sub)。

集群代理

因为 Redis Cluster 内部使用的是P2P中的Gossip协议,每个节点既可以从其他节点得到服务,也可以向其他节点提供服务,没有中心的概念,通过一个节点可以获取到整个集群的所有信息。

所以如果应用连接Redis Cluster可以配置一个节点地址,也可以配置多个节点地址。但需要注意如果集群进行了上下节点的的操作,其应用也需要进行修改,这样会导致需要重启应用,非常的不友好。

从Redis 6.0开始支持了Prxoy,可以直接用Proxy来管理各个集群节点。

本文来介绍下如何使用官方自带的proxy:redis-cluster-proxy 通过使用 redis-cluster-proxy 可以与组成Redis集群的一组实例进行通讯,就像是单个实例一样。Redis群集代理是多线程的,使用多路复用通信模型,因此每个线程都有自己的与群集的连接,该连接由属于该线程本身的所有客户端共享。

在某些特殊情况下(例如MULTI事务或阻塞命令),多路复用将被禁用;并且客户端将拥有自己的集群连接。这样客户端仅发送诸如GET和SET之类的简单命令就不需要Redis集群的专有连接。

redis-cluster-proxy的主要功能特点:

路由:每个查询都会自动路由到集群的正确节点

多线程 支持多路复用和专用连接模型

在多路复用上下文中,可以确保查询执行和答复顺序 发生ASK | MOVED错误后自动更新集群的配置:当答复中发生此类错误时,代理通过获取集群的更新配置并重新映射所有插槽来自动更新集群。 更新完成后所有查询将重新执行,因此,从客户端的角度来看,一切正常进行(客户端将不会收到ASK | MOVED错误:他们将在收到请求后直接收到预期的回复)群集配置已更新)。

跨槽/跨节点查询:支持许多命令,这些命令涉及属于不同插槽(甚至不同集群节点)的多个键。这些命令会将查询分为多个查询,这些查询将被路由到不同的插槽/节点。 这些命令的回复处理是特定于命令的。 某些命令(例如MGET)将合并所有答复,就好像它们是单个答复一样。 其他命令(例如MSET或DEL)将汇总所有答复的结果。 由于这些查询实际上破坏了命令的原子性,因此它们的用法是可选的(默认情况下禁用)。 一些没有特定节点/插槽的命令(例如DBSIZE)将传递到所有节点,并且将对映射的回复进行映射缩减,以便得出所有回复中包含的所有值的总和。 可用于执行某些特定于代理的操作的附加PROXY命令

Disque module

这个本来是作者几年前开发的一个基于 Redis 的消息队列工具,但多年来作者发现 Redis 在持续开发时,他也要持续把新的功能合并到这个Disque 项目里面,这里有大量无用的工作。因此这次他在 Redis 的基础上通过 Modules 功能实现 Disque。 如果业务并不需要保持严格消息的顺序,这个 Disque 能提供足够简单和快速的消息队列功能

其他

新的Expire算法:用于定期删除过期key的函数activeExpireCycle被重写,以便更快地收回已经过期的key;

提供了许多新的Module API;

从服务器也支持无盘复制:在用户可以配置的特定条件下,从服务器现在可以在第一次同步时直接从套接字加载RDB到内存;

SRANDMEMBER命令和类似的命令优化,使其结果具有更好的分布;

重写了Systemd支持;

官方redis-benchmark工具支持cluster模式;

提升了RDB日志加载速度;

运维监控

如何理解Redis监控呢 如何理解Redis监控呢 Redis可视化监控工具 Redis监控体系 监控体系化包含哪些维度? 具体的监控指标有哪些呢?

Redis大厂经验

Redis在微博的应用场景 Redis在微博的优化 未来展望