文章目录

  • 一、初识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。
  1. 基于内存操作,内存读写速度快。
  2. 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))、数字(整数、浮点数),甚至是二进制(图片、音频、视频)。

  

python redis 字符转义_字符串


  Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。

  当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。自增值超过最大值,会报错。

2.1.2 哈希(存储键值对)

  哈希的Value是个键值对,即{field1,value1}一个哈希最多可以存储2的32次方 -1个元素。示例:

python redis 字符转义_缓存_02


  哈希类型中的映射关系叫作field-value

  和操作String的命令相比,对hash的很多操作命令是在String的操作命令之前加了h

  Redis的字典相当于Java的HashMap,它是无序字典。内部实现结构上同Java的HashMap也是一致的,同样的数组+链表二维结构。第一维hash的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

python redis 字符转义_缓存_03


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

python redis 字符转义_缓存_04


  渐进式rehash会在rehash的同时,保留新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务中以及hash的子指令中,循序渐进地将旧hash的内容一点点迁移到新的hash结构中。

  当hash移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

  hash结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash可以对用户结构中的每个字段单独存储。这样当需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

  哈希表渐进式rehash的详细步骤:

  1. 为ht[1]分配空间, 让字典同时持有ht[0]和ht[1]两个哈希表。
  2. 在字典中维持一个索引计数器变量rehashidx, 并将它的值设置为0,表示rehash工作正式开始。
  3. 在rehash进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1] , 当rehash工作完成之后, 程序将rehashidx属性的值增1。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0]的所有键值对都会被rehash至ht[1] , 这时程序将rehashidx属性的值设为-1 , 表示rehash操作已完成。

  渐进式rehash的好处在于它采取分而治之的方式, 将rehash键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式rehash而带来的庞大计算量。

2.1.3 列表(存储有序字符串)

  列表用来存储多个有序的字符串,一个列表最多可以存储2的32次方 -1个元素

  在Redis中,可以对列表两端插入和弹出,还可以获取指定范围的元素列表、获取指定索引下标的元素等。

  列表:按照插入顺序的字符串链表(双向链表),主要命令是LPUSH和RPUSH,能够支持反向查找和遍历。

python redis 字符转义_缓存_05


  列表有两个特点:有序、可重复

  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),这样就可以为成员排序,并且插入是有序的。

python redis 字符转义_字符串_06


  有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能。列表、集合和有序集合三者的异同点:

数据结构

是否允许重复元素

是否有序

有序实现方式

应用场景

列表



索引下标

消息队列等

集合




标签等

有序集合



分值

排行榜系统等

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

  如果用哈希类型来缓存的话:

python redis 字符转义_Redis_07


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

python redis 字符转义_Redis_08

  • 在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 beijing
2.2.3 列表的使用场景(消息队列/复杂对象列表)

  list——因为list是有序的,比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写入的时间来排序,如:最新的XXX,消息队列等。

  • 1、消息队列
      Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

BRPOP和RPOP命令相似,唯一的区别就是当列表没有元素时BRPOP命令会一直阻塞连接,直到有新元素加入。

python redis 字符转义_字符串_09

  • 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会自动删除,多用于一些有时限的场合,其典型应用场景:

  1. 设置限制的优惠活动的信息;
  2. 一些及时需要更新的数据,如:积分排行榜;
  3. 手机验证码的时间;
  4. 限制网站访客访问频率
2.3.2 秒杀系统的简单设计
  1. 提前预热数据,放入Redis缓存

  商品列表放入List;
  商品的详情数据 Hash保存,设置过期时间;
  商品的库存数据Zset保存;
  用户的地址信息Set保存;

  1. 订单产生扣库存通过Redis制造分布式锁,库存同步扣除;
  2. 订单产生后发货的数据,通过消息队列处理;
  3. 秒杀结束后,再把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 beijing
2.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)发布消息,订阅该频道的每个客户端都可以收到对应的消息。
  • python redis 字符转义_字符串_10

  • 使用Stream
      Redis5.0 中增加了一个数据类型Stream,它借鉴了Kafka的设计,是一个新的强大的支持多播的可持久化的消息队列。
      每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。
  • python redis 字符转义_缓存_11

  •   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)。

python redis 字符转义_缓存_12


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

python redis 字符转义_python redis 字符转义_13


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

python redis 字符转义_python redis 字符转义_14


  这种链表加多级索引的结构,就是跳表。

  跳表这个动态数据结构,不仅支持查找操作,还支持动态的插入、删除操作,而且插入、删除操作的时间复杂度也是 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 football
2.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更新值并未成功):

python redis 字符转义_Redis_15


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

python redis 字符转义_缓存_16

  • 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命令的使用示例:

python redis 字符转义_缓存_17


  除了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个元素示例:

python redis 字符转义_Redis_18

  • 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的元素:

python redis 字符转义_字符串_19

  • 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",示例:

python redis 字符转义_python redis 字符转义_20

  • 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 memberzrevrank 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"

  在使用重命名命令时,注意事项:

  1. 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性,这点不要忽视。
  2. 如果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:键不存在。

  示例:

python redis 字符转义_字符串_21


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

python redis 字符转义_python redis 字符转义_22

  • 9、查看键的数据结构类型
      命令是type key,示例:
  • python redis 字符转义_python redis 字符转义_23


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种做法:

  1. 在一个不对外提供服务的Redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制。
  2. 如果确认键值总数确实比较少,可以执行keys命令。
  3. 使用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 上的事件类型来选择对应的事件处理器进行处理。

python redis 字符转义_python redis 字符转义_24


  文件事件处理器的结构包含 4 个部分:

多个 socket;
IO 多路复用程序;
文件事件分派器;
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。

  多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。

  Redis基于Reactor(事件驱动模型) 模式开发了自己的网络事件处理器,使用I/O多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器

  Redis的处理流程:

python redis 字符转义_redis_25


  Redis客户端对服务端的每次调用都经历了【发送命令执行命令返回结果】三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。

  多个客户端发送的命令的执行顺序是不确定的,但是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。

  文件事件处理器是单线程的,所以Redis才叫做单线程的模型。

4.2 Redis为什么这么快*

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

  用图表示多路 I/O 复用模型的流程,如下:

python redis 字符转义_字符串_26

  • 4、高效的数据结构
      Redis每种数据类型底层都做了优化,目的就是为了追求更快的速度。
  • 5、C 语言实现
      一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。

4.3 Redis为何选择单线程*

  避免过多的上下文切换开销:程序始终运行在进程中单个线程内,没有多线程切换的场景。 &emsp;&emsp;避免同步机制的开销:如果 Redis选择多线程模型,需要考虑数据同步的问题,则必然会引入某些同步机制,会导致在操作数据过程中带来更多的开销,增加程序复杂度的同时还会降低性能。
  实现简单,方便维护`:如果 Redis使用多线程模式,那么所有的底层数据结构的设计都必须考虑线程安全问题,那么 Redis 的实现将会变得更加复杂。