文章目录
- 一、初识Redis
- 1.1 【Redis特性(性能好/数据类型丰富/可以持久化/高可用/键值对的存储方式)】
- 1.2 Redis的优缺点
- 1.3 为什么要用缓存
- 1.4 Redis和其他容器的比较*
- 1.5 【Redis的使用场景(缓存/排行榜/计数器/消息队列)】
- 二、数据类型
- 2.1 基础数据类型(最多可以存储2的32次方 -1个元素)
- 2.1.1 字符串(最大长度为512M)
- 2.1.2 哈希(存储键值对)
- 2.1.3 列表(存储有序字符串)
- 2.1.4 集合(存储无序字符串)
- 2.1.5 有序集合(存储排序后的字符串)
- 2.2 不同数据类型的应用场景
- 2.2.1 【字符串的使用场景(json/计数器/分布式锁)】
- 2.2.2 哈希的使用场景(详情)
- 2.2.3 列表的使用场景(消息队列/复杂对象列表)
- 2.2.4 集合的使用场景(标签)
- 2.2.5 有序集合的使用场景(排行榜)
- 2.3 Redis数据类型的相关问题
- 2.3.1 【Redis中对于过期时间的应用(优惠活动/验证码)】
- 2.3.2 秒杀系统的简单设计
- 2.3.3 在Redis中存对象的几种方式
- 2.3.4 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来(scan命令)
- 2.3.5 一个Redis实例最多能存放多少的key(2的32次方)
- 2.3.6 【怎么使用Redis做异步队列(List/发布订阅/Stream)】
- 2.3.7 【Redis如何实现延时队列(ZSET,时间戳作为score)】
- 2.3.9 SortedSet和List异同点
- 2.3.10 【为什么Redis的操作是原子性的,怎么保证原子性的】
- 2.3.12 Redis如何做内存优化
- 2.3.13 跳表
- 2.3.14 都有哪些办法可以降低 Redis 的内存使用情况
- 2.4 键值设计
- 2.4.1 key名设计
- 2.4.2 value设计
- 2.4.3 控制key的生命周期
- 三、常用命令
- 3.1 字符串命令(set设置/get获取/mset批量设置/mget批量获取/incr自增/decr自减)
- 3.2 哈希命令*
- 3.3 列表命令(lpush左侧插入/rpush右侧插入/lpop左侧弹出/rpop右侧弹出/blpop阻塞式左侧弹出/brpop阻塞式右侧弹出)
- 3.4 集合命令*
- 3.5 有序集合命令 *
- 3.6 key管理命令
- 3.6.1 key管理(rename重命名/expire键过期/persist键永不过期/exists检查是否存在/del删除键/ttl查看键剩余过期时间)
- 3.6.2 遍历键(keys/scan渐进式遍历)
- 3.7 容器型数据结构的通用规则
- 3.8 修改配置不重启 Redis 会实时生效吗
- 四、Redis工作原理
- 4.1 Redis线程模型*
- 4.2 Redis为什么这么快*
- 4.3 Redis为何选择单线程*
本系列文章:
Redis(一)数据类型、常用命令
Redis(二)Redis的高级特性、客户端的基本使用、持久化 Redis(三)主从复制、哨兵、集群 Redis(四)缓存、分布式锁
一、初识Redis
Redis是一个使用C语言编写的,开源的高性能非关系型(NoSQL)的键值对数据库。
Redis中存储的是键值对,常用的值类型有5种:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。
Redis将所有数据都存放在内存中,所以读写性能非常好,Redis每秒可以处理超过10万次读写操作,是已知性能最快的Key-Value DB。因此Redis被广泛应用于缓存方向。
Redis还可以将内存的数据利用快照和日志的形式进行持久化。
Redis 也经常用来做分布式锁。
1.1 【Redis特性(性能好/数据类型丰富/可以持久化/高可用/键值对的存储方式)】
- 1、速度快
官方给出的数字是读写性能可以达到10万/秒。 - 2、存储数据的方式是键值对
Redis键值对中的值,主要用来存储5种数据结构:字符串、哈希、列表、集合、有序集合。 - 3、丰富的功能
除了5种数据结构,Redis还提供了许多额外的功能:
1、提供了
键过期功能,可以用来实现缓存。
2、提供了发布订阅功能,可以用来实现消息系统。
3、支持Lua脚本功能,可以利用Lua创造出新的Redis命令。
4、提供了简单的事务功能,能在一定程度上保证事务特性。
5、提供了流水线功能,这样客户端能将一批命令一次性传到Redis服务端,减少了网络的开销。
- 4、简单稳定
Redis使用单线程模型。 - 5、持久化
Redis提供了两种持久化方式:RDB和AOF,即可以用两种策略将内存的数据保存到物理设备中。 - 6、主从复制
Redis提供了复制功能,实现了多个相同数据的Redis副本,复制功能是分布式Redis的基础。 - 7、高可用和分布式
Redis从2.8版本正式提供了高可用实现Redis Sentinel(哨兵),能够保证Redis节点的故障发现和故障自动转移。Redis从3.0版本正式提供了分布式实现Redis Cluster(集群),是Redis真正的分布式实现,提供了高可用、读写和容量的扩展性。
1.2 Redis的优缺点
【Redis的优点】
- 1、读写性能优异
Redis能读的速度是110000次/s,写的速度是81000次/s。
- 基于内存操作,内存读写速度快。
- Redis是单线程的,避免线程切换开销及多线程的竞争问题。单线程是指网络请求使用一个线程来处理,即一个线程处理所有网络请求,Redis 运行时不止有一个线程,比如数据持久化的过程会另起线程。
- 2、支持数据持久化
支持AOF和RDB两种持久化方式。 - 3、支持事务
Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。 - 4、数据结构丰富
除了支持string类型的value外,还支持hash、set、zset、list等数据结构。 - 5、支持主从复制
主机会自动将数据同步到从机,可以进行读写分离。
【Redis的缺点】
- 1、数据库容量受到物理内存的限制,不能用作海量数据的高性能读写
Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 - 2、Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂
为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
1.3 为什么要用缓存
Redis经常被用来做缓存,那么为什么要用缓存呢?主要从“高性能”和“高并发”这两点来看待这个问题:
- 1、高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。此时将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。数据库中的对应数据改变之后,同步改变缓存中相应的数据即可。 - 2、高并发
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
1.4 Redis和其他容器的比较*
【Redis和Map】
- 1、
Redis可以用几十G内存来做缓存,Map不行,一般JVM也就分几个G数据就够大了。 - 2、Redis的缓存可以
持久化,Map实现的是本地缓存,最主要的特点是轻量以及快速,但是它们的生命周期随着jvm的销毁而结束。 - 3、Redis可以实现
分布式的缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。Map实现的是本地缓存,不具有一致性。 - 4、Redis可以处理
每秒百万级的并发,是专业的缓存服务,Map只是一个普通的对象。 - 5、Redis缓存有
过期机制,Map无此功能。
【Redis和Memcached】
- 1、支持的数据类型
Memcached本质上是一个键值存储系统,支持存储任意二进制数据,支持的数据类型就是字符串;
Redis除了常见的字符串、哈希、列表、集合和有序集合,还支持位图(Bitmap)、HyperLogLog和地理空间 (Geospatial)。 - 2、是否支持持久化
MemCached不支持数据持久化,重启后数据会消失;
Redis支持数据持久化。 - 3、速度
Redis的速度比Memcached快很多。 - 4、使用的IO模型
Memcached使用多线程的非阻塞IO模型;
Redis使用单线程的多路IO复用模型。 - 5、存储的数据大小
Redis 最大可以达到 1gb;memcache 只有 1mb。
1.5 【Redis的使用场景(缓存/排行榜/计数器/消息队列)】
- 缓存
缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加快数据的访问速度,而且能够有效地降低后端数据源的压力。 - 排行榜
Rdis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。 - 计数器
Redis天然支持计数功能而且计数的性能也非常好。 - 社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存这种类型的数据,Redis提供的数据结构可以相对比较容易地实现这些功能。 - 消息队列
Rdis提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足。
二、数据类型
- Redis基本数据类型
String:最常用的一种数据类型,String类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB。
Hash:Hash是一个键值对集合。
Set:无序去重的集合。Set提供了交集、并集等方法,对于实现共同好友、共同关注等功能特别方便。
List:有序可重复的集合,底层是依赖双向链表实现的。
SortedSet:有序Set。内部维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。 - Redis特殊数据类型
Bitmap:位图,是一种特殊的字符串数据类型,它利用字符串类型键(key)来存储一系列连续的二进制位(bits),每个位可以独立地表示一个布尔值(0 或 1)。这种数据结构非常适合用于存储和操作大量二值状态的数据,尤其在需要高效空间利用率和特定位操作场景中表现出色。位图有以下应用场景:
用户在线状态跟踪:用一个位表示一个用户的在线状态(1 表示在线,0 表示离线),用户ID作为偏移量。
用户签到系统:记录用户每天的签到情况,每位对应一天,偏移量对应日期。
访问统计:记录网站页面、广告点击等的访问次数,每个二进制位代表一次访问。
数据去重:利用位图快速判断某个整数值是否已存在于集合中,避免重复记录。
大范围计数:如统计某段时间内活跃用户数、订单数等,通过位图进行高效计数。
Hyperloglog。HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。典型的使用场景是统计一个大型网站的去重日活、月活用户(如果通过 set 集合、bitmap 这类常用工具,但有个最大的缺点是,如果数据量巨大,比如 1 亿,甚至 10 亿将耗费巨大内存消耗。HyperLogLog,是一种概率性的统计算法,每个 HyperLogLog 对象最大占用空间为 12KB,相当节省内存。)。
Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。Geospatial主要用在需要地理位置的应用上。将指定的地理空间位置(经度、纬度、名称)添加到指定的 key 中,这些数据将会存储到 sorted set。这样的目的是为了方便使用 GEORADIUS 或者 GEORADIUSBYMEMBER 命令对数据进行半径查询等操作。也就是说,推算地理位置的信息,两地之间的距离,周围方圆的人等等场景都可以用它实现。
2.1 基础数据类型(最多可以存储2的32次方 -1个元素)
Redis主要有5种数据类型,包括String、List、Hash、Set、Zset,可以满足大部分的使用要求。
数据类型 | 常用命令 | 可以存储的值 | 操作 | 应用场景 |
STRING | set,get,decr,incr,mget 等 | 字符串、整数或者浮点数 ;字符串形式的对象 | 对整个字符串或者字符串的其中一部分执行操作;
| 做简单的键值对缓存。 常规计数:微博数,粉丝数等。 |
LIST | lpush,rpush,lpop,rpop,lrange | 列表(固定顺序) | 从两端压入或者弹出元素; 对单个或者多个元素进行修剪,只保留一个范围内的元素 |
|
SET | sadd,spop,smembers,sunion | 无序集合 | 添加、获取、移除单个元素; 检查一个元素是否存在于集合中; 计算交集、并集、差集; 从集合里面随机获取元素 |
|
HASH | hget,hset,hgetall 等 | 包含键值对的无序散列表 | 添加、获取、移除单个键值对; 获取所有键值对; 检查某个键是否存在 | 结构化的数据,比如一个对象。可以用Hash数据结构来存储用户信息,商品信息等等 |
ZSET | zadd,zrange,zrem,zcard | 有序集合(顺序可以动态改变) | 添加、获取、删除元素; 根据分值范围或者成员来获取元素; 计算一个键的排名 |
|
2.1.1 字符串(最大长度为512M)
字符串是Redis最基础的Value类型。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频)。

Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。
当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。自增值超过最大值,会报错。
2.1.2 哈希(存储键值对)
哈希的Value是个键值对,即{field1,value1}。一个哈希最多可以存储2的32次方 -1个元素。示例:

哈希类型中的映射关系叫作field-value。
和操作String的命令相比,对hash的很多操作命令是在String的操作命令之前加了h。
Redis的字典相当于Java的HashMap,它是无序字典。内部实现结构上同Java的HashMap也是一致的,同样的数组+链表二维结构。第一维hash的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

不同的是,Redis的字典的值只能是字符串,另外它们rehash的方式不一样,因为Java的HashMap在字典很大时,rehash是个耗时的操作,需要一次性全部rehash。Redis为了高性能(作为单线程的 Redis 很难承受这样耗时的过程),不能堵塞服务,所以采用了渐进式rehash策略。

渐进式rehash会在rehash的同时,保留新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务中以及hash的子指令中,循序渐进地将旧hash的内容一点点迁移到新的hash结构中。
当hash移除了最后一个元素之后,该数据结构自动被删除,内存被回收。
hash结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash可以对用户结构中的每个字段单独存储。这样当需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。
哈希表渐进式rehash的详细步骤:
- 为ht[1]分配空间, 让字典同时持有ht[0]和ht[1]两个哈希表。
- 在字典中维持一个索引计数器变量rehashidx, 并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1] , 当rehash工作完成之后, 程序将rehashidx属性的值增1。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0]的所有键值对都会被rehash至ht[1] , 这时程序将rehashidx属性的值设为-1 , 表示rehash操作已完成。
渐进式rehash的好处在于它采取分而治之的方式, 将rehash键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式rehash而带来的庞大计算量。
2.1.3 列表(存储有序字符串)
列表用来存储多个有序的字符串,一个列表最多可以存储2的32次方 -1个元素。
在Redis中,可以对列表两端插入和弹出,还可以获取指定范围的元素列表、获取指定索引下标的元素等。
列表:按照插入顺序的字符串链表(双向链表),主要命令是LPUSH和RPUSH,能够支持反向查找和遍历。

列表有两个特点:有序、可重复。
Redis的列表相当于Java中的LinkedList,意味着列表的插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为O(n)。
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进Redis的列表,另一个线程从这个列表中轮询数据进行处理。
2.1.4 集合(存储无序字符串)
集合类型也可以用来保存多个字符串,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
Set:用哈希表类型的字符串序列,没有顺序,集合成员是唯一的,没有重复数据,底层主要是由一个value为null的Hashmap来实现的。
一个集合最多可以存储(2的32次方 -1)个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
Redis的集合相当于Java语言里面的HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值NULL。当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
2.1.5 有序集合(存储排序后的字符串)
有序集合保留了集合不能有重复成员的特性,并且给每个元素设置一个分数(score)用来排序。一个有序集合最多可以存储(2的32次方 -1)个元素。
Zset类似于 Java 中 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。
Zset:和set类型基本一致,不过它会给每个元素关联一个double类型的分数(score),这样就可以为成员排序,并且插入是有序的。

有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能。列表、集合和有序集合三者的异同点:
数据结构 | 是否允许重复元素 | 是否有序 | 有序实现方式 | 应用场景 |
列表 | 是 | 是 | 索引下标 | 消息队列等 |
集合 | 否 | 否 | 否 | 标签等 |
有序集合 | 否 | 是 | 分值 | 排行榜系统等 |
2.2 不同数据类型的应用场景
2.2.1 【字符串的使用场景(json/计数器/分布式锁)】
string——适合最简单的k-v存储,短信验证码缓存、限流、计数器、分布式锁、分布式Session等,就用这种类型来存储。
- 1、json对象
String类型最常见的就是存序列化后的json对象了,用作查询对象的缓存。
较典型的缓存使用场景:Redis作缓存层,MySQL作存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到提高读写性能和降低后端压力的作用。示例:
在使用Redis做缓存时,就涉及到了key的设计。简单来说,key的设计,可以参考表名:主键名:主键值:列名。 - 2、计数器
Redis作为计数的基础工具,可以实现快速计数(并将数据异步存储到数据库)的功能。例如用户每播放一次视频,相应的视频播放数就会自增1;点赞数也是同理。 - 3、限速
很多网站出于安全考虑,会在登录时,让用户输入手机验证码,并限制用户每分钟获取验证码的频率,伪代码: - 4、分布式锁
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用Redis自带的setnx命令实现分布式锁,还可以使用官方提供的RedLock分布式锁实现。
2.2.2 哈希的使用场景(详情)
hash一般key为ID或者唯一标示,value对应的就是详情了。如商品详情、个人信息详情、新闻详情、文章等。
比如有关系型数据表记录的两条用户信息:
id | name | age | city |
1 | zhangsan | 25 | luoyang |
2 | lisi | 26 | kaifeng |
如果用哈希类型来缓存的话:

相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。比如可以将每个用户的id定义为键后缀,多对field-value对应每个用户的属性。伪代码:

- 在Redis中存储详细信息的3种方式
1、原生字符串。每个属性一个键,示例:
set user:1:name tom
set user:1:age 23
set user:1:city beijing 优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
2、序列化字符串。将用户信息序列化后用一个键保存,示例:
set user:1 serialize(userInfo) 优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
3、哈希。每个用户属性使用一对field-value,但是只用一个键保存。示例:
hmset user:1 name tomage 23 city beijing2.2.3 列表的使用场景(消息队列/复杂对象列表)
list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的XXX,消息队列等。
- 1、消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
BRPOP和RPOP命令相似,唯一的区别就是
当列表没有元素时BRPOP命令会一直阻塞连接,直到有新元素加入。

- 2、复杂对象列表
以文章为例,每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
每篇文章使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content。
向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键:
2.2.4 集合的使用场景(标签)
set——可以简单的理解为ID-List的模式,如微博中一个人有哪些好友,set最牛的地方在于,可以对两个set提供交集、并集、差集操作。例如:赞、踩、标签、查找两个人共同的好友等。
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
2.2.5 有序集合的使用场景(排行榜)
Sorted Set——是set的增强版本,增加了一个score参数,自动会根据score的值进行排序。比较适合类似于top 10等不根据插入的时间来排序的数据(排行榜)。
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数等。
2.3 Redis数据类型的相关问题
2.3.1 【Redis中对于过期时间的应用(优惠活动/验证码)】
Redis中可以使用expire命令设置一个键的生存时间,到时间后Redis会自动删除,多用于一些有时限的场合,其典型应用场景:
- 设置限制的优惠活动的信息;
- 一些及时需要更新的数据,如:积分排行榜;
- 手机验证码的时间;
- 限制网站访客访问频率。
2.3.2 秒杀系统的简单设计
- 提前预热数据,放入Redis缓存;
商品列表放入List;
商品的详情数据 Hash保存,设置过期时间;
商品的库存数据Zset保存;
用户的地址信息Set保存;
- 订单产生扣库存通过Redis制造分布式锁,库存同步扣除;
- 订单产生后发货的数据,通过消息队列处理;
- 秒杀结束后,再把Redis缓存数据和数据库同步。
2.3.3 在Redis中存对象的几种方式
- 1、原生字符串类型(每个属性一个键)
set user:1:name tom
set user:1:age 23
set user:1:city beijing 优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
- `2、序列化字符串类型(将用户信息序列化后用一个键保存)【推荐】
set user:1 serialize(userInfo) 优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
将对象转换为json字符串后进行存储的方式,也是类似。
- 3、哈希类型(每个用户属性使用一对field-value,但是只用一个键保存)
hmset user:1 name tom age 23 city beijing2.3.4 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来(scan命令)
使用keys命令可以扫出指定模式的key列表。如果这个Redis正在给线上的业务提供服务,那使用keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕服务才能恢复。
这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,会有一定的重复概率,在客户端做一次去重就可以了。
用scan所花费的时间,比用keys所花费的时间长。
2.3.5 一个Redis实例最多能存放多少的key(2的32次方)
理论上Redis可以处理多达232 的 keys,并且在实际中进行了测试,每个实例至少存放了2亿5千万的keys。
任何list、set、和 sorted set都可以放232个元素。换句话说,Redis的存储极限是系统中的可用内存值。
2.3.6 【怎么使用Redis做异步队列(List/发布订阅/Stream)】
- 使用List结构作为队列(一边存数据,另一边取数据)
队列:基于List结构的消息队列,是一种 Publisher 与 Consumer 点对点的强关联关系。
rpush(从右侧插入消息)生产消息,lpop(从左侧取出消息)消费消息。当lpop没有消息的时候,要适当sleep一段时间,然后再检查有没有信息。这是一种 Publisher 与 Consumer 点对点的强关联关系。
如果不想sleep的话,可以使用blpop,在没有信息的时候,会一直阻塞,直到信息的到来。示例:
public class RedisQueueExample {
private static final String HOST = "localhost";
private static final int PORT = 6379;
private static final String QUEUE_NAME = "myqueue";
public static void main(String[] args) {
// 连接到Redis服务器
Jedis jedis = new Jedis(HOST, PORT);
// 发布消息到队列
jedis.lpush(QUEUE_NAME, "message1", "message2", "message3");
// 从队列中拉取消息
while (true) {
String message = jedis.rpop(QUEUE_NAME);
if (message == null) {
// 队列为空,可以暂停或者睡眠
try {
Thread.sleep(500); // 休眠500毫秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
// 处理消息
System.out.println("Processing message: " + message);
// 处理完毕后,可以选择删除消息
// jedis.del(message);
}
}
// 关闭连接
jedis.close();
}
}- 使用通道channel(发布订阅)
Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到对应的消息。 - 使用Stream
Redis5.0 中增加了一个数据类型Stream,它借鉴了Kafka的设计,是一个新的强大的支持多播的可持久化的消息队列。
每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。 -
Consumer Group:消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer), 这些消费者之间是竞争关系。
last_delivered_id:游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
pending_ids:消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。
消息ID:消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的ID要大于前面的消息ID。
消息内容: 消息内容就是键值对,形如hash结构的键值对。 - redis发布订阅(消息可能会丢失)与Stream模式(可以持久化)的区别
PubSub:生产者传递过来一个消息,Redis会直接找到相应的消费者传递过去。如果一个消费者都没有,那么消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。
Stream:能提供持久性、顺序的队列功能,实现顺序队列实现的内部结构。


2.3.7 【Redis如何实现延时队列(ZSET,时间戳作为score)】
利用redis的ZSET集合 (有序集合)数据结构就可以实现一个简单的延迟队列。
redis的zset数据结构中的每个元素都有一个分数score和一个值value,我们可以将任务的执行时间戳作为score,将任务数据作为value,将任务插入到zset中,每个任务有一个唯一的id(比如订单id),以及任务执行时间(比如30min),任务内容(比如订单超时支付系统自动取消)等信息体。
然后另起一个线程,该线程会周期性地从zset中取出score最小(即最早要执行的)的任务,如果该任务的score小于当前时间戳,则执行任务,否则等待一段时间再次检查,直到任务可以执行,执行任务后,通过Redis的remove命令删除已经成功执行的任务即可。
2.3.9 SortedSet和List异同点
- 相同点
1、都是有序的;
2、都可以获得某个范围内的元素。 - 不同点
1、列表基于链表实现,获取两端元素速度快,访问中间元素速度慢;
2、有序集合基于散列表和跳跃表实现,访问中间元素时间复杂度是O(logN);
3、列表不能简单的调整某个元素的位置,有序列表可以(更改元素的分数);
4、有序集合更耗内存。
2.3.10 【为什么Redis的操作是原子性的,怎么保证原子性的】
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
- 多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua的方式实现。
2.3.12 Redis如何做内存优化
尽可能使用散列表,散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称、姓氏、邮箱、密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。
2.3.13 跳表
对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)。

怎么来提高查找效率呢?如果像图中那样,对链表建立一级“索引”,查找起来是不是就会更快一些呢?每两个结点提取一个结点到上一级,我们把抽出来的那一级叫做索引或索引层。

上面是增加一层索引减少的查找次数,如果增加多层,自然也就相应减少更多了。如下图:

这种链表加多级索引的结构,就是跳表。
跳表这个动态数据结构,不仅支持查找操作,还支持动态的插入、删除操作,而且插入、删除操作的时间复杂度也是 O(logn)。这个主要体现了链表的优势,当知道相应的位置之后,只需要做对应的链接就行。
- 跳表的应用场景
Redis的有序列表就会使用到跳表作为底层的数据结构。
比如以下场景:
1、插入一个数据;
2、删除一个数据;
3、查找一个数据;
4、按照区间查找数据(比如查找值在[100, 356]之间的数据);
5、迭代输出有序序列。
应对上面的场景,红黑树也可以做到O(logn)的时间复杂度。但是由于树的结构不太好顺序查找,因此,“按照区间查找数据(比如查找值在[100, 356]之间的数据)”使用跳表效率更高。对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。
除此之外,Redis 之所以用跳表来实现有序集合,还有其他原因,比如,跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。还有,跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
2.3.14 都有哪些办法可以降低 Redis 的内存使用情况
如果你使用的是 32 位的 Redis 实例,可以好好利用 Hash,list,sorted set,set 等集合类型数据,因为通常情况下很多小的 Key-Value 可以用更紧凑的方式存放到一起。
2.4 键值设计
2.4.1 key名设计
- 1、【建议】: 可读性和可管理性
以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id,示例:ugc:video:1。 - 2、【建议】:简洁性
保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,示例:user:{uid}:friends:messages:{mid}简化为u:{uid}:fr:m:{mid}。 - 3、【强制】:不要包含特殊字符
反例:包含空格、换行、单双引号以及其他转义字符。
2.4.2 value设计
- 1、【强制】:拒绝太大的字符串
string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。 - 2、【推荐】:选择适合的数据类型
反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football正例:
hmset user:1 name tom age 19 favor football2.4.3 控制key的生命周期
建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime(空闲时间)。
三、常用命令
Redis有5种常用的数据类型,有一些命令时通用的,同时对于不同的数据类型,也有不同的命令。
3.1 字符串命令(set设置/get获取/mset批量设置/mget批量获取/incr自增/decr自减)
- 1、设置值
命令是set key value [ex seconds] [px milliseconds] [nx|xx]。示例:
set命令有几个选项:
ex seconds:为键设置秒级过期时间。
px milliseconds:为键设置毫秒级过期时间。
nx:键必须不存在,才可以设置成功,用于添加键值对。
xx:与nx相反,键必须存在,才可以设置成功,用于更新键值对。
除了set选项,Redis还提供了setex和setnx两个命令。它们的作用和ex和nx选项是一样的。示例(hello键存在,所以用setnx更新值并未成功):

用setex命令是可以更新键成功的,更新的同时并设置了一下键过期时间。如以下例子是更新键值,并设置过期时间为60秒:

- 2、获取值
命令是get key:
如果要获取的键不存在,则返回nil(空)。 - 3、批量设置值
命令是mset key value [key value ...],示例: - 4、批量获取值
命令是mget key [key ...],示例:
批量操作命令可以提高开发效率。在不使用mget这样的命令时,执行n次get命令需要的时间为n次get时间 = n次网络通信时间(一来一回) + n次命令执行时间:
使用mget命令后,要执行n次get命令需要的时间为:n次get时间 = 1次网络通信时间 + n次命令执行时间:
Redis可以支撑每秒数万的读写操作,这里的指的是Redis服务端的处理能力。
对于客户端来说,每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞。 - 5、计数
命令是incr key,incr命令用于对值做自增操作,返回结果分为三种情况:
1、值不是整数,返回错误。
2、值是整数,返回自增后的结果。
3、键不存在,按照值为0自增,返回结果为1。
incr命令的使用示例:

除了incr命令,Redis还提供了decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数)。
decr命令使用示例:
# 对存在的数字值 key 进行 DECR
redis> SET failure_times 10
OK
redis> DECR failure_times
(integer) 9
# 对不存在的 key 值进行 DECR
redis> EXISTS count
(integer) 0
redis> DECR count
(integer) -1
# 对存在但不是数值的 key 进行 DECR
redis> SET company YOUR_CODE_SUCKS.LLC
OK
redis> DECR company
(error) ERR value is not an integer or out of range- 6、追加值
命令是append key value,示例: - 7、获取字符串长度
命令是strlen key,单位是字节,示例:
下面操作返回结果为6,因为每个中文占用3个字节: - 8、设置并返回原值
命令是getset key value,示例:
3.2 哈希命令*
- 1、设置值
命令是hset key field value,示例: - 2、获取值
命令是hget key field,示例: - 3、删除field
命令是hdel key field [field ...],返回结果为成功删除field的个数,示例: - 4、计算field个数
命令是hlen key,示例: - 5、批量设置、批量获取
批量设置的命令是hmset key field value [field value ...];批量获取的命令是hmget key field [field ...]**。
hmset和hmget分别是批量设置和获取field-value,示例 - 6、判断field是否存在
命令是hexists key field,存在时结果为1,否则结果为0,示例: - 7、获取所有field
命令为hkeys key,作用为返回指定哈希键所有的field,示例: - 8、获取所有value
命令是hvals key,示例: - 9、获取所有的field-value
命令是hgetall key,示例:
在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令。 - 10、计算value的字符串长度
命令是hstrlen key field。例如hget user:1name的value是tom,那么hstrlen的返回结果是3。
3.3 列表命令(lpush左侧插入/rpush右侧插入/lpop左侧弹出/rpop右侧弹出/blpop阻塞式左侧弹出/brpop阻塞式右侧弹出)
列表的四种操作类型:
操作类型 | 操作 |
添加 | rpush lpush linsert |
查找 | lrange lindex llen |
删除 | lpop rpop lrem ltrim |
修改 | lset |
阻塞操作 | blpop brpop |
- 1、从右边插入元素
命令是rpush key value [value ...],示例:
从左边插入元素的命令是lpush key value [value ...],使用方法和rpush相同,只不过从左侧插入。 - 2、向某个元素前或者后插入元素
命令是linsert key before|after pivot valuelinsert命令会从列表中找到等于pivot的元素,在其前(before)或者后(after)插入一个新的元素value,示例: - 3、获取指定范围内的元素列表
命令是lrange key start end,lrange操作会获取列表指定索引范围所有的元素。索引下标有两个特点:
第一,索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
第二,lrange中的end选项包含了自身。
获取列表的第2到第4个元素示例:

- 4、获取列表指定索引下标的元素
命令是lindex key index,获取当前列表最后一个元素示例:慢操作:lindex相当于Java链表的get(int index)方法,它需要对链表进行遍历,性能随着参数index增大而变差。 ltrim和字面上的含义不太一样,个人觉得它叫lretain(保留) 更合适一些,因为ltrim跟的两个参数start_index和end_index定义了一个区间,在这个区间内的值,ltrim要保留,区间之外统统砍掉。我们可以通过ltrim来实现一个定长的链表,这一点非常有用。index可以为负数,index=-1表示倒数第一个元素,同样 index=-2表示倒数第二个元素。 - 5、获取列表长度
命令是llen key。示例: - 6、从列表左侧弹出元素、从列表右侧弹出元素
从列表左侧弹出元素的命令是lpop key,从列表右侧弹出的命令是rpop key。示例: - 7、删除指定元素
命令是lrem key count value,lrem命令会从列表中找到等于value的元素进行删除**,根据count的不同分为三种情况:
count>0,从左到右,删除最多count个元素。
count<0,从右到左,删除最多count绝对值个元素。
count=0,删除所有。
假如当前列表变为“a a a a a java b a”,下面操作将从列表左边开始删除4个为a的元素:

- 8、按照索引范围修剪列表
命令是ltrim key start end,只保留列表第2个到第4个元素示例: - 9、修改指定索引下标的元素
命令是lset key index newValue,将列表中的第3个元素设置为python示例:
3.4 集合命令*
- 1、添加元素
命令是sadd key element [element ...],结果为添加成功的元素个数,示例: - 2、删除元素
命令是srem key element [element ...],结果为成功删除元素个数,示例: - 3、计算元素个数
命令是scard key。scard的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用Redis内部的变量,示例: - 4、判断元素是否在集合中
命令是sismember key element,如果给定元素element在集合内返回1,反之返回0,示例: - 5、随机从集合返回(最多)指定个数元素
命令是srandmember key [count]**,[count]是可选参数,如果不写默认为1,示例: - 6、从集合随机弹出元素
命令是spop key。srandmember和spop都是随机从集合选出元素,两者不同的是spop命令执行后,元素会从集合中删除,而srandmember不会。 - 7、获取所有元素
命令是smembers key,返回结果是无序的。
smembers和lrange、hgetall都属于时间复杂度较高的命令,如果元素过多存在阻塞Redis的可能性,可以使用sscan来完成。
此时假如有两个集合:myset1里的元素是"1 2 3",myset2里的元素是"2 3 4",示例:

- 8、求多个集合的交集
命令是sinter key [key ...],示例: - 9、求多个集合的并集
命令是suinon key [key ...],示例: - 10、求多个集合的差集
命令是sdiff key [key ...],示例:
3.5 有序集合命令 *
- 1、添加成员
命令是zadd key score member [score member ...]。
向有序集合user:ranking添加用户tom和他的分数251示例: - 2、计算成员个数
命令是zcard key。 - 3、计算某个成员的分数
命令是zscore key member,获取value的score示例: - 4、计算成员的排名
命令是zrank key member和zrevrank key member,zrank是从分数从低到高返回排名,zrevrank反之。示例: - 5、删除成员
命令是zrem key member [member ...],返回结果为成功删除的个数。 - 6、增加成员的分数
命令是zincrby key increment member,给tom增加了9分,分数变为了260分示例: - 7、返回指定排名范围的成员
命令是zrange key start end [withscores]和zrevrange key start end [withscores],有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。
返回排名最低的是三个成员,如果加上withscores选项,同时会返回成员的分数示例: - 8、返回指定分数范围的成员
命令是zrangebyscore key min max [withscores] [limit offset count]和zrevrangebyscore key max min [withscores] [limit offset count],zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。示例: - 9、返回指定分数范围成员个数
命令是zcount key min max,示例: - 10、删除指定排名内的升序元素
命令是zremrangebyrank key start end,示例: - 11、删除指定分数范围的成员
命令是zremrangebyscore key min max。将250分以上的成员全部删除,返回结果为成功删除的个数示例:
3.6 key管理命令
3.6.1 key管理(rename重命名/expire键过期/persist键永不过期/exists检查是否存在/del删除键/ttl查看键剩余过期时间)
- 1、键重命名
命令为rename key newkey。
如果在rename之前,键java已经存在,那么它的值也将被覆盖,示例:
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> set c d
OK
127.0.0.1:6379> rename a c
OK
127.0.0.1:6379> get a
(nil)
127.0.0.1:6379> get c
"b"为了防止被强行rename,Redis提供了renamenx命令,确保只有newKey不存在时候才被覆盖,例如下面操作renamenx时,newkey=python已经存在,返回结果是0代表没有完成重命名,所以键java和python的值没变:
127.0.0.1:6379> set java jedis
OK
127.0.0.1:6379> set python redis-py
OK
127.0.0.1:6379> renamenx java python
(integer) 0
127.0.0.1:6379> get java
"jedis"
127.0.0.1:6379> get python
"redis-py"在使用重命名命令时,注意事项:
- 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性,这点不要忽视。
- 如果rename和renamenx中的key和newkey如果是相同的,在Redis3.2和之前版本返回结果略有不同。Redis3.2中会返回OK,Redis3.2之前的版本会提示错误。
- 2、随机返回一个键
命令为randomkey。 - 3、键过期
除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令。
expire key seconds:键在seconds秒后过期。
expireat key timestamp:键在秒级时间戳timestamp后过期。
ttl命令和pttl都可以查询键的剩余过期时间,但是pttl精度更高可以达到毫秒级别,有3种返回值:
大于等于0的整数:键剩余的过期时间(ttl是秒,pttl是毫秒)。
-1:键没有设置过期时间。
-2:键不存在。
expireat命令可以设置键的秒级过期时间戳。
除此之外,Redis2.6版本后提供了毫秒级的过期方案:
pexpire key milliseconds:键在milliseconds毫秒后过期。pexpireat key milliseconds-timestamp:键在毫秒级时间戳timestamp后过期。
但无论是使用过期时间还是时间戳,秒级还是毫秒级,在Redis内部最终使用的都是pexpireat。
在使用Redis相关过期命令时的注意事项:
1、如果expire key的键不存在,返回结果为0。
2、如果过期时间为负值,键会立即被删除,犹如使用del命令一样。
3、persist命令可以将键的过期时间清除(即数据永久有效)。
4、对于字符串类型键,执行set命令会去掉过期时间。下面的例子证实了st会导致过期时间失效,因为tt1变为-1。
5、Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能,例如不能对列表类型的一个元素做过6时间设置。
6、setex命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间。- 4、查看键总数
命令是dbsize: - 5、检查键是否存在:
命令是exists key,0和1代表是否存在指定key(exists不支持通配符)。示例: - 6、删除键
命令是del key [key ...]。del是一个通用命令,无论值是什么数据结构类型,del命令都可以将其删除。0和1代表是否成功删除指定key,示例:
del命令还可以支持删除多个键。示例: - 7、对键添加过期时间
命令是expire key seconds,单位为秒。当超过过期时间后,会自动删除键。0和1代表是否设置成功,示例: - 8、查看键的剩余过期时间:
命令是ttl,有3种返回值:
大于等于0的整数:键剩余的过期时间,单位为秒。-1:键没设置过期时间。-2:键不存在。
示例:

对于字符串类型键,执行set命令会去掉该key的过期时间。示例:

- 9、查看键的数据结构类型:
命令是type key,示例:

3.6.2 遍历键(keys/scan渐进式遍历)
Redis提供了两个命令遍历所有的键,分别是keys和scan。
- 1、全量遍历键
命令为keys pattern。简单使用示例:
127.0.0.1:6379> keys *
1) "hill"
2) "jedis"
3) "redis"如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境下使用keys命令。有时候确实需要遍历键,有以下3种做法:
- 在一个不对外提供服务的Redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制。
- 如果确认键值总数确实比较少,可以执行keys命令。
- 使用scan命令渐进式的遍历所有键,可以有效防止阻塞。
- 2、渐进式遍历
scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。
scan cursor [match pattern] [count number]cursor:表示遍历的起始游标,第一次调用时通常使用 0。
MATCH pattern(可选):用于指定匹配的模式,只返回与给定模式匹配的元素。模式可以包含通配符 * 和 ?。
COUNT count(可选):指定一次返回的元素数量,用于控制每次返回的元素数量,减少网络传输。
SCAN命令会返回一个数组,其中第一个元素是下一次迭代的游标值,后续元素是匹配的元素列表。
用法示例:
SCAN 0 # 返回数据库中所有元素
SCAN 0 MATCH key* # 返回以 "key" 开头的所有元素
SCAN 0 COUNT 10 # 返回最多 10 个元素SCAN使用示例:
redis 127.0.0.1:6379> scan 0
1) "17"
2) 1) "key:12"
2) "key:8"
3) "key:4"
4) "key:14"
5) "key:16"
6) "key:17"
7) "key:15"
8) "key:10"
9) "key:3"
10) "key:7"
11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
2) "key:18"
3) "key:0"
4) "key:2"
5) "key:19"
6) "key:13"
7) "key:6"
8) "key:9"
9) "key:11" 在上面的示例中,第一次调用使用 0 作为游标来开始一次新的迭代。第二次调用时使用上一次调用返回的游标,即命令回复的第一个元素值,即17。从上面的示例可以看到,SCAN 命令返回值是两个值的数组:第一个值是下一次调用中将要使用的新游标,第二个值是包含返回元素的数组。
由于在第二次调用中返回的游标为 0,因此服务器向调用者发送信号,告知迭代已完成,并且遍历完数据集。从游标值 0 开始迭代,然后调用 SCAN 直到返回的游标再次为 0,表示一个完整迭代。
3.7 容器型数据结构的通用规则
list/set/hash/zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
- 1 、create if not exists
如果容器不存在,那就创建一个,再进行操作。比如rpush操作刚开始是没有列表的,Redis就会自动创建一个,然后再rpush进去新元素。 - 2 、drop if no elements
如果容器里元素没有了,那么立即删除元素,释放内存。这意味着lpop操作到最后一个元素,列表就消失了。
3.8 修改配置不重启 Redis 会实时生效吗
针对运行实例,有许多配置选项可以通过 CONFIG SET 命令进行修改,而无需执行任何形式的重启。 从 Redis 2.2 开始,可以从 AOF 切换到 RDB 的快照持久性或其他方式而不需要重启 Redis。检索 ‘CONFIG GET *’ 命令获取更多信息。
但偶尔重新启动是必须的,如为升级 Redis 程序到新的版本,或者当你需要修改某些目前CONFIG 命令还不支持的配置参数的时候。
四、Redis工作原理
4.1 Redis线程模型*
Redis 内部使用文件事件处理器,这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型。Redis采用 IO 多路复用机制同时监听多个 socket,将产生事件的socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:
多个 socket;
IO 多路复用程序;
文件事件分派器;
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
Redis基于Reactor(事件驱动模型) 模式开发了自己的网络事件处理器,使用I/O多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。
Redis的处理流程:

Redis客户端对服务端的每次调用都经历了【发送命令,执行命令,返回结果】三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。
多个客户端发送的命令的执行顺序是不确定的,但是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
文件事件处理器是单线程的,所以Redis才叫做单线程的模型。
4.2 Redis为什么这么快*
- 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速
像Mysql这样的成传统关系型数据库是索引文件存储来内存,数据文件存储在硬盘的,那么硬盘的性能和瓶颈将会影响到数据库。
内存和硬盘的读写方式不相同,导致了它们在读写性能上的巨大差异。 - 2、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU
这里的单线程指的是,Redis处理网络请求的时候只有一个线程,而不是整个Redis服务是单线程的。
Redis4.0之前一直采用单线程的主要原因有以下三个:
- 使用单线程模型,Redis的开发和维护更简单,因为单线程模型方便开发和调试;
- 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是多路复佣和非阻塞 IO;
- 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非CPU。
- 3、使用多路I/O复用模型,非阻塞IO
这里的I/O指的是网络I/O,多路指的是多个网络连接,复用指的是复用一个线程。Redis使用多路 I/O 复用模型的大致流程如下:
- 在Redis中的I/O多路复用程序会
监听多个客户端连接的Socket。- 每当有客户端通过Socket流向Redis发送请求进行操作时,I/O多路复用程序会
将请求放入一个队列。- 同时I/O多路复用程序会
同步、有序、每次传送一个任务给处理器处理。- I/O多路复用程序会在上一个请求处理完毕后再继续分派下一个任务。
用图表示多路 I/O 复用模型的流程,如下:

- 4、高效的数据结构
Redis每种数据类型底层都做了优化,目的就是为了追求更快的速度。 - 5、C 语言实现
一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
4.3 Redis为何选择单线程*
避免过多的上下文切换开销:程序始终运行在进程中单个线程内,没有多线程切换的场景。   避免同步机制的开销:如果 Redis选择多线程模型,需要考虑数据同步的问题,则必然会引入某些同步机制,会导致在操作数据过程中带来更多的开销,增加程序复杂度的同时还会降低性能。
实现简单,方便维护`:如果 Redis使用多线程模式,那么所有的底层数据结构的设计都必须考虑线程安全问题,那么 Redis 的实现将会变得更加复杂。
















