引言
特性:
- 拥有丰富的数据结构和操作
- 支持集群
- 有高效的持久化机制
- Redis 在对 100k下的数据存储效率更高
- Redis用单线程方式处理事件,但使用多路IO复用模型来监听多个 socket 连接
为什么使用单进程单线程?如何处理多个客户端的 socket 连接?
Redis 是单线程模型。不过,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求,但默认也是单线程来进行操作。
官网对使用单线程模型的解释:
Redis is single threaded. How can I exploit multiple CPU / cores?
It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, using pipelining Redis running on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.
However, to maximize CPU usage you can start multiple instances of Redis in the same box and treat them as different servers. At some point a single box may not be enough anyway, so if you want to use multiple CPUs you can start thinking of some way to shard earlier.
You can find more information about using multiple Redis instances in the Partitioning page.
However with Redis 4.0 we started to make Redis more threaded. For now this is limited to deleting objects in the background, and to blocking commands implemented via Redis modules. For the next releases, the plan is to make Redis more and more threaded.
重点解释:因为cpu并不是Redis的瓶颈,在单线程模式的情况下已经很快了,就没有必要再使用多线程了。
总结下来有几点,使得 Redis 使用单线程来完成操作:
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
为什么数据操作效率高
-
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速,类似于HashMap,其查找和操作的时间复杂度都是O(1);
-
采用单线程,避免了多线程的上下文切换和竞争条件
-
使用了 I/O 多路复用模型(也叫事件驱动模型)
- 多路I/O复用模型是利用 select、poll、epoll 同时监察多个 IO 流的 FD 是否就绪,把多个 IO 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。由于 select 函数是顺序扫描 FD 是否就绪,效率比较低,Linux 提供了另外一个 epoll系统调用,基于事件驱动方式 代替 顺序扫描,因此性能更高,是 Redis 中使用到的。
- 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用 I/O 多路复用 技术可以让单个线程高效的处理多个连接请求(减少了等待网络 IO 数据的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈。
网络 I/O 模型参考:BIO、NIO、AIO
一、 Redis 数据类型与应用:
1.0.1 字符串:String(SET key value)
- 按二进制存储,也就是可以接受任何类型格式的数据;
- 规定了一个字符串 value 最大可为 512M
应用场景:
-
存储热点的单个字符串或者 JSON 串
-
由于是二进制安全的,可以存储图片文件到 value 中
-
线程安全的计数器(如使用 INCR key 来完成点赞或文章阅读数的修改)
-
用作分布式锁(SET…NX 或 SETNX …)
1.0.2 可重复列表:List (LSET key index value):
简单的字符串列表,按照插入顺序排序(可用该特性完成需要的数据结构),允许重复元素;
可以 添加 / 删除 一个元素在列表的头部(LPUSH key value [value …] 、 LPOP key)或者列表的尾部(RPUSH key value [value …] 、 RPOP key),也可以删除列表中的值(LREM key count value)
应用场景:
-
使用 BLPOP key timeout 在timeout时间内等待弹出队首元素,来实现一个任务队列、不太成熟的 消息队列。
-
在分布式机器中,将日志记录到 Redis 中的 List 中,然后按顺序记录到一个日志文件中。
1.0.3 哈希表:Hashs(HSET key field value):
可以看成具有键值对的map容器
应用场景:
- 用来存储对象的键值对。如果使用字符串类型来存储 JSON 字符串对象,JSON 的序列化/反序列化会带来性能上的消耗,且在修改 JSON 字符串对象时,需考虑线程安全问题。如果使用哈希表来存储对象中的键值对,不会有序列化和线程安全问题,且占用内存小。
1.0.4 不重复集合:Set(SADD key member [member …]):
没有排序的字符集合,集合中不允许出现重复的元素
应用场景:
-
使用 SRANDMEMBER key [count] 从集合中随机取出 count 个元素,可用作热点数据分类的随机推送
-
使用 SINTER / SUNION / SDIFF 求两个集合的交、并、差,来实现对应的数据要求。
1.0.5 有序不重复集合:SortedSet(ZADD key score member [score] [member]):
带有分数的set集合,并按升序排列;
其中的元素是唯一的, 但分数(score)是可以重复的;
score使用64位存储,整数范围为 正负9*10^18,双精度 double 需要注意精度的丢失
应用场景:
-
对数据进行一个排序,并可以在列表中拿到最大,最小的多少个元素等操作。
-
实现定时的消息发送,使用 score 存储需要发送消息的时间戳,服务端轮询 SortedSet 中的数据判断是否要发送通知。
1.0.6 位图:SETBIT key offset value
将数据的唯一标识信息,直接或通过某一个函数间接 映射到位图二进制中的某一位上,用 0和1表示一个状态。
该类型的数据结构,可用于大量数据的统计计算。
应用场景:
- 对在全年内没有播放的电影数量进行统计:将每一条电影数据创建一个位图数据,如果某一天有播放记录,则将对应位图中的位置修改为1,然后对每部电影产生的一个 BitMaps 进行或运算(BITOP or storeKey key1 key2…),得到的 storeKey 中位为 0 的就是没播放的电影。
对 BitMap 的升级数据结构: Bloom Filter、Cuckoo Filter
1.0.7 HyperLogLog
HyperLogLog是用作基数(集合中,不重复元素的个数)统计的,内部使用了loglog算法。该种数据结构的操作只有添加元素、获取基数个数、合并多个HyperLogLog。
注意点:
-
HyperLogLog中,并不会存储添加的具体数据,只是记录集合的基数
-
基数是算法估算的结果,会有0.81%的误差,但是该数据结构一般用在数据量非常大的场景,故该误差对系统影响可忽略
-
HyperLogLog占用的空间一定不会超过12KB,占用空间少
1.0.8 地理位置:GEOADD key longitude latitude member [longitude …]
上述 api 中,key表示地图集、longitude表示经度、latitude表示纬度、member表示当前坐标点名称。
应用场景:
- 使用 GEORADIUSBYMEMBER key member radius m|km|ft|mi 求得已存在点 member 附近 radius 内所有的点,可用于查找附近的人。同样也可以使用 GEORADIUS 命令实现。
1.0.9 发布订阅模型:publish / subscribe
它不基于任何数据类型,也没有做任何的数据存储(即不会用 AOF 或 RDB),仅仅是一个实时的数据转发通道。
应用场景:
- 支持发布 / 订阅,支持多组生产者、多组消费者处理实时的消息,在哨兵集群、Redissioin红锁下有使用到
- 有以下几种消息丢失情况,不适合能做消息队列:
- 该模型没有持久化,Redis 宕机后消息会丢失
- 消息堆积到上限后,会丢失
- 同一个消费者无法重复消费同一条消息
1.0.10 Stream
Redis 5.0 及之后推出的较为成熟的消息队列结构。
优缺点:
- 支持消费者阻塞获取消息,发布 / 订阅模式
- 支持重复消费
- 避免因服务宕机,而导致的消息丢失(但AOF的间隔写入日志或Redis集群节点切换可能会丢失少部分数据)
- 消息堆积会导致消息丢失
1.1 Hash 数据结构
上图是 Redis 中的 hash 结构,其对应关系为:
一个 RedisDB 中,对应一个 dict,一个 dict 对应两个 dictht(扩容期间使用两个),一个 dictht 包含一个散列通(存放多个 dictEntry)。
扩、缩容机制与其带来的影响
rehash:
在 Java 中 HashMap 扩容是个很耗时的操作,需要先申请新的数组空间,然后在一个循环中更新所有元素到新数组中。在 Redis 中为了追求高性能扩、缩容,采用了 渐进式 rehash 策略,来改变 dictht 大小。
渐进式 rehash 采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。渐进式 rehash 的详细流程可参看 这里。
扩、缩容条件:
当元素的个数等于数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。当 dictht 中元素个数少于数组长度的 10%时,Redis 会进行缩容来减少空间占用。
影响:
从 rehash 的详细流程中可以看到,rehash 第一步需要申请内存空间用于存放 dictht[1],dictht[1] 申请的内存大小如下图。如果 rehash 时内存不够,Redis 将执行驱逐策略,Master/Slave 将会把大量的 Key 驱逐淘汰,导致 Master/Slave 主从不一致,带来其他业务影响。
该问题的解决办法之一,可以监控集群内存的使用情况,及时处理或提前运营规避,其他关于渐进式 rehash 的问题可参考 这里
1.2 SortedSet 中的跳跃表
1.2.1 什么是跳跃表
跳跃表是一种类似链表的有序的数据结构,目的是为了降低单向链表查找的时间复杂度,类似于红黑树,但跳跃表的范围查找效率较高。
1.2.2 跳跃表的实现
链表内每一个结点包含多个指向后续元素的指针,是通过随机得到的。每次查找从顶层开始向下对比查找,便可以跳过一些元素
1.3 基于 BitMap 实现的两种过滤器
Bloom Filter(布隆过滤器)
结构:
BF 由一串二进制(0或1)向量和一系列随机映射函数组成
功能:
用于检索一个元素是否在一个集合中,BF 可以确定一个数据 一定不在集合中。
优 / 缺点:
空间效率高,占用空间少; 查询时间短
有一定的误判率; 元素不能删除(因为不能判断某一个位具体被多少个函数映射到了)
BF 主要有两种实现方式,依赖 Redis(Redission) 和不依赖 Redis(即应用内存,Guava) 。
借助 Redis 实现的 Bloom Filter 与 BitMap 类似,但 BitMap 的数据映射关系由用户定义,Bloom Filter 的映射关系函数由算法内部定义,并有多个函数共同工作。
Cuckoo Filter(布谷鸟过滤器)
上文提到 BF 存在一个致命的缺点,不能删除元素,只能在程序认为误差率已经不能接受时进行重建。
CF 使用特殊的函数,解决了这个问题,让集合中的元素可以删除,但也存在一定的误差率。
二、 持久化机制
2.1 RDB持久化机制
-
可以在配置文件中修改刷入的规则,或者存储的文件路径和文件名。dump.rdb 路径默认为 “./”,即在那里启动,就在那里生成文件,一般固定一个文件夹。
-
是Redis默认的持久化方式,使用快照方式,按刷入的规则,使用bgsave,将对数据的增删改操作记录自动刷入到磁盘文件中
-
也可以手动调用 save / bgsave 命令来手动触发,两个方式使用主进程 / fork产生的子进程完成数据的存储;如果使用的save,那么单进程单进程的 Redis 会阻塞其他 I/O 请求;如果使用的是bgsave,则需再分配与父进程相同大小的资源空间,并产生一个临时文件,数据写入完成后,临时文件替换掉 dump.rdb。
Redis默认配置情况下,开启的是 RDB Snapshotting,只有满足了配置的刷入规则,才会在配置的路径,向/*.rdb的文件记录数据操作
Q&A:
什么时候触发 RDB 机制?
- 当未开启 AOF 时,客户端执行 shutdown 命令时,触发 bgsave 的保存操作
- 满足 redis.conf 中配置的规则时,触发保存操作
- 执行 save、bgsave 命令时,触发保存操作
2.2 AOF持久化机制
- 默认是不开启的,需要配置appendonly yes,使用日志记录所有的修改操作
- 三种触发方式
appendfsync always # 每次都同步(最安全但是最慢)
appendfsync everysec #每秒同步(AOF默认的同步策略)
appendfsync no #不主动同步,由操作系统来决定(最快但是不安全)
- AOF 中有对日志重写的机制(bgrewriteaof命令),改操作将对 *.aof 文件中的记录进行一个合并,如当文件中记录有两条 set k v,set k v2 时,将被合并为一条。相关配置如下:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb 当文件大小达到64MB的时候,触发bgrewriteaof命令。生产环境可设置GB为单位。
Redis默认的AOF是关闭,开启后,默认每秒记录操作到磁盘上的数据文件,向 /*.aof 文件按固定的格式追加 Redis 操作命令,如 set 等。
2.3 RDB与AOF的对比
- 在发生意外宕机的情况下,RDB丢失的数据多于AOF,AOF在默认的每秒同步情况下,最多丢失1秒的数据
- AOF的文件存储可阅读的字符串,RDB产生不可阅读的二进制文件。故 RDB 恢复速度快,存储效率高。
- *.rdb 文件和 *.aof 文件同时存在时,采用 *.aof 文件中的记录
三、 事务
使用 multi 开启事务,使用 exec 提交事务,使用 discard 丢弃事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name snail
QUEUED
127.0.0.1:6379> get nane
QUEUED
127.0.0.1:6379> set name snail111
QUEUED
127.0.0.1:6379> exec
1) OK
2) (nil)
3) OK
- 开启 multi 后面的指令,都会被添加到一个队列中,在遇到 exec 时,将期间的操作一起提交执行
- 在开启 multi 后,如果遇到语法错误的指令,那么事务将被自动丢弃;如果是语法正确的指令在执行过程中出现问题,那么事务会被成功执行,错误指令返回对应的提示。
其他资料:
JavaGuide-Redis
Coding-Redis