Redis开发与运维

本书全面讲解了Redis基本功能及其使用,并结合线上开发与运维中的实际案例,深入分析并总结了实际工作中遇到的“陷进”,以及背后的原因,包含大规模集群开发与管理的场景、应用案例与开发技巧,为高效开发运维提供了大量实际经验和建议。

本书不要求读者有任何Redis使用经验,对入门与进阶DevOps的开发者提供有价值的帮助。

主要内容包括:

  • Redis的安装配置
  • API
  • 各种高效功能
  • 客户端
  • 持久化
  • 复制
  • 高可用
  • 内存
  • 哨兵
  • 集群
  • 缓存设计等
  • Redis高可用集群解决方案
  • Redis设计和使用中的问题
  • 最后提供了一个开源工具:Redis监控运维云平台CacheCloud
前言

Redis作为基于 键值对NoSQLNot Only Sql)数据库,

具有 高性能丰富的数据结构持久化高可用分布式等特性,

同时Redis本身非常稳定,已经得到业界的广泛认可和使用。

掌握Redis已经逐步称为开发和运维人员的必备技能之一。

​ 本书关注了Redis开发运维的方方面面,尤其对于开发运维中如何提高效率、减少可能遇到的问题进行详细分析,但本书不单单介绍怎么解决这些问题,而是通过对Redis重要原理的解析,帮助开发运维人员学会找到问题的方法,以及理解背后的原理,从而让开发运维人员不仅知其然,而且知其所以然

本书涵盖内容

​ 第1章

​ 初始Redis,带读者进入Redis的世界,了解它的前世今生、众多特性、应用场景、安装配置、简单使用,最后对Redis发展过程中的重要版本进行说明,可以让读者对Redis有一个全面的认识。

​ 第2章

​ API的理解和使用,全面介绍了Redis的 5种数据结构 字符串string)、哈希hash)、列表list)、集合set)、有序集合zset)的 数据模型、常用命令、典型应用场景,并且每个小结都会给出在Redis开发过程中可能要注意的坑和技巧。同时本章还会对Redis的 单线程处理机制、**键值管理 ** 做一个全面介绍,通过对这些原理的理解,读者可以在合适的应用场景选择合适的数据结构和命令进行开发,有效提高程序效率,降低可能产生的问题和隐患。

​ 第3章

​ 小功能大用处,除了 5种数据结构 外,Redis还提供了诸如 慢查询Redis ShellPipelineLua脚本BitmapsHyperLogLog发布订阅GEO 等附加功能,在这些功能的帮助下,Redis的应用场景更加丰富。

​ 第4章

​ 客户端,本章重点关注Redis客户端的开发,介绍了Redis的 客户端通信协议、详细讲解了Java客户端 Jedis 的使用技巧,同时通过从原理角度剖析在开发运维中,客户端的监控和管理技巧,最后给出客户端开发中常见问题以及案例讲解。

​ 第5章

持久化,Redis的 持久化 功能有效避免因进程退出造成的数据丢失问题,本章首先介绍 RDBAOF 两种 持久化配置和运行流程,其次对常见的持久化问题进行定位和优化,最后结合Redis常见的单机多实例部署场景进行优化。

​ 第6章

复制,在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,用于故障恢复和负载均衡等需求,Redis也是如此。它为我们提供了 复制replication)功能,实现了多个相同数据的Redis副本。 复制功能是 高可用Redis的基础,后面章节的 哨兵集群 都是在 复制的基础上 实现高可用

​ 第7章

​ Redis的噩梦:阻塞,Redis是典型的单线程架构所有的读写操作 都在一条 主线程 中完成的。当Redis用于 高并发 场景时这条线程就变成了它的生命线。如果出现阻塞哪怕是很短时间对于我们的应用来说都是噩梦。导致阻塞问题的场景大致分为 内在原因外在原因,本章将进行详细分析。

​ 第8章

​ 理解内存,Redis所有的数据在于内存中,如何高效利用Redis内存变得非常重要。高效利用Redis内存 首先需要理解 Redis内存消耗在哪里如何管理内存,最后再深入到 如何优化内存。掌握这些知识后相信读者能够实现 用更少的内存存储更多的数据 从而 降低成本

​ 第9章

哨兵,Redis从 2.8 版本开始正式提供了 Redis Sentinel,它有效解决了 主从模式故障转移 的若干问题,为Redis提供了 高可用 功能。本章将一步步解析 Redis Sentinel 的相关概念安装部署配置命令使用、原理解析,最后分析了 Redis Sentinel 运维中的一些问题。

​ 第10章

集群,是本书的重头戏, Redis ClusterRedis 3 提供的 Redis 分布式解决方案,有效解决了 Redis分布式 方面的需求,理解应用好 Redis Cluster 将极大地解放我们对 分布式Redis 的需求,同时它也是学习 分布式存储 的绝佳案例。本章将对 Redis Cluster数据分布搭建集群节点通信请求路由集群伸缩故障转移 等方面进行分析说明。

​ 第11章

缓存设计缓存 能够有效 加速应用的读写速度,以及 降低后端负载,对于开发人员进行日常应用的开发至关重要,但是将 缓存 加入应用架构后也会带来一些问题,本章将介绍 缓存使用设计 中遇到的问题,具体包括:缓存的收益和成本缓存更新策略缓存粒度控制穿透问题优化无底洞问题优化雪崩问题优化热点key优化

​ 第12章

​ 开发运维的“陷进”,介绍Redis开发运维中的一些棘手问题,具体包括:Linux配置优化flush误操作数据恢复、如何让Redis变得安全bigkey 问题、热点key 问题。

​ 第13章

Redis监控运维云平台 CacheCloud,介绍笔者所在团队 开源Redis运维工具 CacheCloud,它有效解决了Redis监控和运维中的一些问题,本章将按照 快速部署机器部署接入应用用户功能运维功能 多个维度全面的介绍 CacheCloud,相信在它的帮助下,读者可以更好的监控和运维好Redis。

​ 第14章

​ Redis配置统计字典,会对Redis的 系统状态信息 以及 全部配置 做一个全面的梳理,希望本章能够成为 Redis配置统计字典,协助大家分析和解决日常开发和运维中遇到的问题。

第1章 初始Redis

1.1 盛赞Redis

1.2 Redis特性

1.3 Redis使用场景

​ 1.3.1 Redis可以做什么

​ 1.3.2 Redis不可以做什么

1.4 用好Redis的建议

1.5 正确安装并启动Redis

​ 1.5.1 安装Redis

​ 1.5.2 配置、启动、操作、关闭Redis

1.6 Redis重大版本

1.7 本章重点回顾

第1章 初始Redis

​ 本章将带领读者进入Redis的世界,了解它的前世今生、众多特性、典型应用场景安装配置如何好用 等,最后会对Redis发展过程中的 重要版本 进行说明,本章主要内容如下:

  • 盛赞Redis
  • Redis特性
  • Redis使用场景
  • 用好Redis的建议
  • 正确安装启动Redis
  • Redis重大版本
1.1 盛赞Redis

Redis官网:http://redis.io

Redis 是一种基于 键值对key-value)的 NoSQL 数据库

与很多 键值对数据库 不同的是,

Redis 中的 可以是由 string字符串)、hash哈希)、list列表)、set(集合)、zset有序集合)、Bitmaps位图)、HyperLogLogGEO地理信息定位) 等 多种数据结构和算法 组成,因此 Redis 可以 满足很多的 应用场景

而且因为 Redis 会将 所有数据存放在 内存 中,所以它的 读写性能 非常 惊人

不仅如此,Redis 还可以将 内存的数据 利用 快照日志 的形式 保存到 硬盘 上,这样在发生类似 断电 或者 机器故障 的时候,内存中的数据 不会 “丢失”

除了上述功能以外,Redis 还提供了 键过期发布订阅事务流水线Lua脚本 等附加功能。

总之,如果在合适的场景使用好 Redis,它就会像一把瑞士军刀一样所向披靡。

2008年,Redis 的作者 Salvatore Sanfilippo 在开发一个叫 LLOOGG 的网站时,需要实现一个 高性能队列功能,最开始是使用 MySQL 来实现的,但后来发现无论怎么优化 SQL语句 都不能使 网站的性能 提高上去,再加上自己囊中羞涩,于是他决定自己做一个专属于 LLOOGG 的数据库,这个就是 Redis的前身

后来,Salvatore SanfilippoRedis 1.0 的源码开放到 GitHub 上,可能连他自己都没想到,Redis 后来如此受欢迎。

假如现在有人问 Redis的作者 都有谁在使用 Redis,我想他可以开句玩笑的回答:还有谁不使用Redis,当然这只是开玩笑,但是从 Redis 的官方公司统计来看,有很多重量级的公司都在使用 Redis,如果单单从体量来统计,新浪微博 可以说是全球最大的 Redis 使用者,除了 新浪微博,还有像 阿里巴巴、腾讯、百度、搜狐、优酷土豆、美团、小米、唯品会等公司都是 Redis 的使用者。

除此之外,许多开源技术想ELK等已经把 Redis 作为它们组件中的重要一环,而且 Redis 会在未来的版本中提供 模板系统 让第三方人员 实现扩展功能,让 Redis 发挥出更大的威力。

所以,可以这么说,熟练使用和运维 Redis 已经成为开发运维人员的一个必备技能。

1.2 Redis特性

​ Redis 之所以受到如此多公司的青睐,必然有之过人之处,下面是关于 Redis 的 8个重要特性

  1. 速度 快

    正常情况下,Redis 执行命令的速度 非常快,官方给出的数字是 读写性能 可以达到 10万/秒,当然这也取决于 机器的性能,但这里先不讨论 机器性能上的差异,只分析一下是什么造就了 Redis 除此之外的 速度,可以大致归纳为以下四点:

    • Redis 的 所有数据 都是 存放在 内存 中的,表1-1是谷歌公司2009年给出的各层级硬件执行速度,所以 把数据放在内存 中是 Redis速度快 的最主要原因。
    • Redis 使用 C语言 实现的,一般来说 C语言 实现的程序 “距离” 操作系统更近,执行速度 相对会更快。
    • Redis 使用了 单线程架构,预防了 多线程 可能产生的 竞争问题。
    • 作者对于 Redis源代码 可以说是 精打细磨,曾经有人评价 Redis 是少有的 集性能和优雅于一身 的 开源代码。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CZFzKuIB-1621301836851)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210513170503130.png)]

    表1-1 谷歌公司给出的 各层级 硬件 执行速度

    层级速度
    L1 cache reference0.5ns
    Branch mispredict5ns
    L2 cache reference7ns
    Mutex lock/unlock25ns
    Main memory reference100ns
    Compress 1K bytes with Zippy3 000ns
    Send 2K bytes over 1Gbps network20 000ns
    Read 1MB sequentially from Memory250 000ns
    Round trip within same datacenter500 000ns
    Disk seek10 000 000ns
    Read 1 MB sequentially from disk20 000 000ns
    Send packet CA->Netherlands->CA150 000 000ns
  2. 基于 键值对 的数据结构 服务器

    几乎所有的 编程语言 都提供了类似 字典 的功能,

    例如Java里的 map、Python里的 dict

    类似于这种 组织数据的方式 叫做 基于键值 的方式

    与很多 键值对 数据库 不同的是,

    Redis 中的 不仅可以是 字符串,而且还可以是 具体的 数据结构

    这样不仅能 便于在许多应用场景的开发,同时也能够 提高开发效率

    Redis全称Remote Dictionary Server

    它主要提供了 5种数据结构字符串哈希列表集合有序集合

    同时在 字符串基础之上 演变 出了 位图Bitmaps)和 HyperLogLog 两种神奇的 “数据结构”

    并且随着 LBSLocation Based Service基于位置服务)的不断发展,Redis 3.2 版本 中加入有关 GEO地理信息定位)的功能,

    总之在这些数据结构的帮助下,开发者可以开发出各种“有意思”的应用。

  3. 丰富的功能

    除了 5种数据结构Redis 还提供了许多额外的功能:

    • 提供了 键过期 功能,可以用来实现 缓存
    • 提供了 发布订阅 功能,可以用来实现 消息系统
    • 支持 Lua脚本 功能,可以利用 Lua 创造出 新的Reds命令
    • 提供了简单的 事务 功能,能在一定程度上保证 事务特性
    • 提供了 流水线Pipeline)功能,这样 客户端 能将 一批命令 一次性 传到 Redis减少了 网络的开销
  4. 简单稳定

    Redis简单 主要表现在 三个方面

    首先,Redis源码 很少早期版本 的代码只有 2万行左右3.0版本 以后由于添加了 集群特性,代码增至 5万行左右,相对于很多 NoSQL数据库 来说代码量相对要少很多,也就意味着普通的开发和运维人员完全可以“吃透”它。

    其次,Redis 使用 单线程模型,这样不仅使得 Redis服务端处理模型 变得简单, 而且也使得 客户端开发 变得简单。

    最后,Redis 不需要依赖于 操作系统 中的类库(例如 Memcache 需要 依赖 libevent 这样的 系统类库),Redis 自己 实现事件处理 的相关功能。

    Redis 虽然很简单,但是不代表它不稳定

    以笔者维护的上千个Redis为例,没有出现过因为Redis自身bug而宕掉的情况。

  5. 客户端语言 多

    Redis 提供了 简单的 TCP通信协议,很多 编程语言 可以很 方便地 接入到 Redis,并且由于 Redis 受到 社区和各大公司的 广泛认可,所以 支持Redis的客户端语言 也非常 ,几乎涵盖了 主流的编程语言,例如Java、PHP、Python、C、C++、Nodejs等,第4章我们将对 Redis的客户端 进行详细说明。

  6. 持久化

    通常看,将 数据 放在 内存 中是 不安全的,一旦发生 断电 或者 机器故障重要的数据 可能就会 丢失,因此 Redis 提供了 两种 持久化 方式RDBAOF,即可以用 两种策略内存的数据 保存到 硬盘中(如图1-1所示),这样就 保证数据的可持久性,第5章我们将对 Redis的持久化 进行详细说明。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1inDcMPh-1621301836855)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210513173702303.png)]

  7. 主从复制

    Redis 提供了 复制 功能,实现了 多个相同数据的Redis副本(如图1-2所示),复制 功能是 分布式Redis的基础。第6章我们将会对 Redis的复制 进行详细说明。

  8. 高可用 和 分布式

    Redis2.8版本 正式提供了 高可用 实现Redis Sentinel,它能够保证 Redis节点故障发现故障自动转移

    Redis3.0版本 正式提供了 分布式 实现 Redis Cluster,它是 Redis 真正的 分布式实现,提供了 高可用读写容量的扩展性

1.3 Redis 使用场景

​ 上节我们已经了解了 Redis 的若干个 特性,本节来看一下 Redis典型应用场景 有哪些?

1.3.1 Redis 可以做什么
  1. 缓存

    缓存机制 几乎在所有的大型网站 都有使用,合理地使用缓存 不仅可以 加快数据的访问速度,而且能够 有效地降低后端数据源的压力

    Redis 提供了 键值过期时间设置,并且也提供了 灵活控制最大内存内存溢出后的淘汰策略

    可以这么说,一个 合理的缓存设计 能够为 一个网站的稳定 保驾护航。第11章将对 缓存的设计与使用 进行详细说明。

  2. 排行榜系统

    排行榜系统 几乎存在于所有的网站,例如 按照热度排名的排行榜,按照发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis 提供了 列表有序集合 数据结构,合理地使用 这些 数据结构 可以很 方便地构建各种排行榜系统。

  3. 计数器应用

    计数器 在网站中的作用 至关重要,例如 视频网站有 播放数、电商网站有 浏览数,为了保证 数据的实时性每一次 播放和浏览 都要做 加1 的操作,如果 并发量很大 对于 传统关系型数据库的性能 是一种挑战。

    Redis 天然支持 计数功能 而且 计数的性能 也非常,可以说是 计数器系统 的重要选择。

  4. 社交网络

    赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是 社交网站的必备功能,由于社交网站 访问量通常比较大,而且 传统的关系型数据库 不太适合保存这种类型的数据,Redis 提供的 数据结构 可以相对比较容易地实现这些功能。

  5. 消息队列系统

    消息队列系统 可以说是 一个大型网站的必备基础组件,因为其具有 业务 解耦非实时业务 削峰等特性。

    Redis 提供了 发布订阅阻塞队列 的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列基本可以满足。

1.3.2 Redis不可以做什么

​ 实际上和任何一门技术一样,每个技术都有自己的 应用场景边界,也就是说 Redis不是 万金油,有很多适合它解决的问题,但是也有很多不适合它解决的问题。

我们可以站在 数据规模数据冷热角度来进行分析。

站在 数据规模 的角度看,数据 可以分为 大规模数据小规模数据

我们知道 Redis数据存放在 内存 中的,虽然现在 内存 已经足够 便宜,但是如果 数据量 非常大,例如每天有几亿的用户行为数据,使用 Redis存储的话,基本上是个 无底洞经济成本 非常的

站在 数据冷热 的角度看,数据 分为 热数据冷数据热数据 通常是指需要 频繁操作的数据,反之为冷数据,

例如对于 视频网站 来说,视频基本信息 基本上在各个业务线都是 经常要操作 的数据,而 用户的观看记录 不一定是经常需要访问的数据,

这里暂且不讨论两者数据规模的差异,

单纯站在 数据冷热的角度 上看,视频信息 属于 热数据用户观看记录 属于冷数据

如果将这些 冷数据 放在 Redis 上,基本上是 对 内存的一种浪费

但是对于一些 热数据 可以放在 Redis加速读写,也可以 减轻后端存储的负载,可以说是事半功倍。

所以,Redis不是 万金油,相信随着我们对 Redis 的逐步学习,能够清楚 Redis 真正的使用场景。

1.4 用好Redis的建议
  1. 切勿当做 黑盒 使用,开发与运维同样重要

    很多使用 Redis 的开发者认为只要会用 API 开发相应的功能就可以,更有甚者认为 Redis 就是 get、set、del,不需要知道 Redis 的 原理。

    但是在我们实际运维和使用 Redis 的过程中发现,很多线上的故障和问题都是由于完全把 Redis 当做 黑盒 造成的,

    如果不了解 Redis的单线程模型,有些开发者会在 上千万个键的Redis上 执行 keys * 操作,

    如果不了解 持久化的相关原理,会在一个 写操作量很大的Redis上 配置自动保存RDB

    而且在很多公司内只有专职的 关系型数据库DBA,并没有 NoSQL 的相关运维人员,也就是说开发者很有可能会自己运维 Redis,对于 Redis 的开发者来说既是好事又是坏事。

    站在好的方面看,开发人员可以通过 运维 Redis 真正了解 Redis 的一些原理,不单纯停留在开发上。

    站在坏的方面看,Redis 的开发人员不仅要支持开发,还要承担运维的责任,而且由于运维经验不足可能会造成线上故障。

    但是从实际经验来看,运维足够规模的 Redis 会对用好 Redis 更加有帮助。

  2. 阅读源码

    我们在前面提到过,Redis开源项目,由于作者对 Redis代码 的极致追求,Redis的代码量 相对于许多 NoSQL数据库 来说是非常 的,也就意味着作为普通的开发和运维人员也是可以 “吃透”的。

    通过阅读优秀的源码,不仅能够加深我们对 Redis 的理解,而且还能提高自身的需求,例如新浪微博在 Redis的早期版本 上做了很多的 定制化 来满足自身的需求,豌豆荚也开源基于 Proxy 的 Redis 分布式 实现 Codis。

1.5 正确安装并启动Redis

通常来说,学习一门技术最好的方法就是实战,所以在学习 Redis 这样一个实战中产生的技术时,首先把它安装部署起来,值得庆幸的是,相比于很多软件和工具部署步骤繁杂,Redis的安装 不得不说是非常 简单,本节我们将学习如何 安装Redis。

注意

在写本书时,Redis 4.0 已经发布 RC版,但是大部分公司还都在使用 3.0 或更早的版本(2.6 或 2.8),本书所讲的内容基于 Redis3.0。

1.5.1 安装Redis
  1. 在Linux上安装Redis

    Redis 能够 兼容 绝大部分的 POSIX 系统,例如 LinuxOS XOpenBSDNetBSDFreeBSD

    其中比较典型的是 Linux 操作系统(例如 CentOS、Redhat、Ubuntu、Debian、OS X等)。

    在 Linux 安装软件通常有 两种方法,

    第一种是通过 各个操作系统的软件管理软件 进行安装,例如 CentOSyum 管理工具,Ubuntuapt

    但是由于 Redis 的更新速度相对较快,而这些管理工具不一定能更新到最新的版本,同时前面提到 Redis的安装 本身不是很复杂,所以一般推荐使用第二种方式:源码的方式 进行安装,整个安装只需一下 六步 即可完成,以 3.0.7 版本 为例:

    wget http://download.redis.io/release/redis-3.0.7.tar.gz
    
    tar xzf redis-3.0.7.tar.gz
    
    ln -s redis-3.0.7 redis
    
    cd redis
    
    make
    
    make install
    
    1. 下载 Redis 指定版本的 源码压缩包 到当前目录。

    2. 解压缩 Redis 源码压缩包。

    3. 建立一个 redis 目录 的软连接,指向 redis-3.0.7

    4. 进入 redis 目录

    5. 编译(编译之前确保操作系统已经安装 gcc

    6. 安装

    这里有 两点 需要注意:

    第一,第3步 中建立了一个 redis 目录的 软链接,这样做是为了 不把 redis 目录 固定在 指定版本上,有利于 Redis 未来版本升级,算是 安装软件 的一种好习惯。

    第二,第6步 中的 安装 是将 Redis 的相关运行文件放到 /usr/local/bin/ 下,这样就可以在任意目录下执行 Redis 的命令。

    例如安装后,可以在任何目录执行 redis-cli -v 查看 Redis 版本。

    redis-cli -v
    redis-cli 3.0.7
    

    注意

    第12章将介绍更多 Linux配置优化 技巧,为 Redis 的良好运行保驾护航。

    1. 在Windows上安装Redis

    Redis 的官方并不支持微软的Windows操作系统,但是 Redis 作为一款优秀的开源技术吸引到了微软公司的注意,微软公司的开源技术组在 GitHub 上维护一个 Redis 的分支:https://github.com/MSOpenTech/redis。

    那为什么 Redis 的作者没有开发和维护对Windows用户的 Redis 版本呢?

    这里可以简单分析一下:

    首先 Redis 的许多 特性 都是和 操作系统 相关的,Windows操作系统 和 Linux操作系统 有很大的区别,所以会 增加维护成本,而且更重要的是大部分公司都在使用 Linux 操作系统,而 Redis 在Linux 操作系统上的表现已经得到了实践的验证。

    对于使用 Windows操作系统 的读者,可以通过安装虚拟机来体验 Redis 的诸多特性。

    注意

    对 Windows版本 的 Redis 感兴趣的读者,可以尝试安装和部署 Windows版本的Redis,但是本书中的知识和例子不能确保在Windows下能够运行。

1.5.2 配置、启动、操作、关闭 Redis

Redis 安装之后,src/usr/local/bin 目录下多了几个以 redis 开头 可执行文件,我们称之为 Redis Shell,这些 可执行文件 可以做很多事情,例如可以启动和停止 Redis、可以 检测和修复 Redis 的持久化文件,还可以 检测 Redis 的性能

表 1-2 中分别列出这些可执行文件的说明。

表 1-2 Redis 可执行文件说明

可执行文件作用
redis-server启动Redis
redis-cliRedis命令行客户端
redis-benchmarkRedis基准测试工具
redis-check-aofRedis AOF持久化文件检测和修复工具
redis-check-dumpRedis RDB持久化文件检测和修复工具
redis-sentinel启动Redis Sentinel

Redis 持久化Redis Sentinel 分别在第5章和第9章才会涉及,Redis基准测试 将在第3章介绍,所以本节只对 redis-serverredis-cli 进行介绍。

  1. 启动 Redis

    三种方法 启动Redis默认配置运行配置配置文件启动

    (1)默认配置

    这种方法会使用 Redis的默认配置 来启动,下面就是 redis-server 执行后输出的相关日志。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bRAOIMuK-1621301836858)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210514142427083.png)]

    可以看到直接使用 redis-server 启动 Redis 后,会打印一些日志,通过日志可以看到一些信息,上例中可以看到:

    • 当前的 Redis版本 是 3.0.7。
    • Redis 的 默认端口 是 6379。
    • Redis 建议要 使用配置文件 来启动。

    因为 直接启动 无法自定义配置,所以这种方式是不会在生产环境中使用的。

    (2)运行启动

    redis-server 加上 要修改配置名和值(可以是多对),没有设置的配置将使用默认配置:

    # redis-server --configKey1 configValue1 --configKey2 configValue2
    

    例如,如果要用 6380 作为 端口 启动 Redis,那么可以执行:

    # redis-server --port 6380
    

    虽然 运行配置 可以 自定义配置,但是如果 需要修改的配置较多 或者 希望将配置保存到文件中,不建议使用这种方式。

    (3)配置文件启动

    将 配置 写到 指定文件 里,例如我们将配置写到了 /opt/redis/redis.conf 中,那么只需要执行如下命令即可启动 Redis:

    # redis-server /opt/redis/redis.conf
    

    Redis 有 60 多个配置,这里只给出一些重要的配置(参见表 1-3),其他配置会随着不断深入学习进行介绍,第14章会将所有的配置说明进行汇总。

    表 1-3 Redis的基础配置

    配置名配置说明
    port端口
    logfile日志文件
    dirRedis 工作目录(存放持久化文件和日志文件)
    daemonize是否以守护进程的方式启动Redis

    运维提示

    Redis 目录下都会有一个 redis.conf 配置文件,里面就是 Redis的 默认配置,通常来讲我们会在 一台机器上启动多个Redis,并且将 配置集中管理在指定目录下,而且 配置不是完全手写的,而是将 redis.conf 作为模板进行修改

    显然通过 配置文件启动的方式 提供了 更大的灵活性,所以大部分生产环境会使用这种方式启动Redis。

  2. Redis 命令行客户端

    现在我们已经启动了 Redis 服务,下面将介绍如何使用 redis-cli 连接,操作 Redis 服务。

    redis-cli 可以使用 两种方式 连接Redis服务器。

    • 第一种是 交互式方式

      通过 redis-cli -h {host} -p {port}的方式连接到 Redis服务,之后所有的操作都是通过 交互的方式 实现,不需要再执行 redis-cli 了,例如:

      redis-cli -h 127.0.0.1 -p 6379
      127.0.0.1:6379> set hello world
      OK
      127.0.0.1:6379> get hello
      "world"
      
    • 第二种是 命令方式

      redis-cli -h ip {host} -p {port} {command}就可以直接得到 命令的返回结果,例如:

      redis-cli -h 127.0.0.1 -p 6379 get hello
      "world"
      

      这里有 两点 要注意:

      1)如果没有 -h 参数,那么 默认连接 127.0.0.1

      ​ 如果没有 -p ,那么 默认 6379 端口,也就是说如果 -h-p 都没写就是连接 127.0.0.1:6379 这个Redis实例。

      2)redis-cli 是学习 Redis 的重要工具,后面的很多章节都是用它作讲解,同时 redis-cli 还提供了很多有价值的参数,可以帮助解决很多问题,有关于 redis-cli 的强大功能将在第3章进行详细介绍。

  3. 停止 Redis 服务

    Redis 提供了 shutdown 命令来 停止Redis服务,例如要停掉 127.0.0.1 上 6379 端口上的 Redis 服务,可以执行如下操作:

    redis-cli shutdown
    

    可以看到 Redis 的日志输出如下:

    # User requested shutdown... # 客户端发出的shutdown命令
    
    * Saving the final RDB snapshot before exiting.
    #保存 RDB 持久化文件(有关 Redis持久化的特性在 1.2节 已经进行了简单的介绍,RDB 是 Redis 的一种 持久化方式)
    
    * DB saved on disk
    # 将 RDB 文件保存在磁盘上
    
    # Redis is now ready to exit, bye bye...
    #关闭
    

    当使用 redis-cli 再次连接该 Redis 服务时,看到 Redis 已经 “失联”。

    redis-cli
    Could not connect to Redis at 127.0.0.1:6379: Connection refused
    

    这里有 三点 需要注意一下:

    1)Redis 关闭的过程断开与客户端的连接持久化文件生成,是一种相对优雅的关闭方式。

    2)除了可以通过 shutdown 命令 关闭Redis服务 以外,还可以通过 kill 进程号 的方式关闭掉 Redis,但是不要 粗暴地 使用 kill -9 强制杀死 Redis服务,不但 不会做 持久化操作,还会 造成 缓冲区等资源不能被优雅关闭,极端情况会造成 AOF和复制丢失数据 的情况。

    3)shutdown 还有一个参数,代表 是否在关闭Redis前,生成持久化文件

    redis-cli shutdown nosave|save
    
1.6 Redis 重大版本

Redis 借鉴了 Linux 操作系统 对于 版本号的命名规则版本号 第二位数 如果是 奇数则为 非稳定版本(例如2.7、2.9、3.1),如果是 偶数则为 稳定版本(例如2.6、2.8、3.0、3.2)。

当前 奇数版本 就是下一个 稳定版本 的 开发版本,例如 2.9版本 是 3.0版本 的开发版本。

所以我们在生产环境通常 选取 偶数版本 的Redis,如果对于某些新的特性想提前了解和使用,可以选择最新的奇数版本。

介绍一门技术的版本是很多技术图书的必备内容,通常读者容易忽略,但随着你对这门技术深入学习后,会觉得“倍感亲切”,而且通常也会关注 新版本的特性,本小节将对 Redis 发展过程中的一些重要版本及特性进行说明。

  1. Redis 2.6

    Redis 2.62012 年正式发布,经历了 17 个版本,到 2.6.17 版本,相比于 Redis 2.4,主要特性如下:

    1)服务端 支持 Lua 脚本

    2)去掉 虚拟内存 相关功能。

    3)放开对 客户端连接数 的硬编码限制。

    4)键 的 过期时间 支持毫秒

    5)从结点 提供 只读功能

    6)两个新的 位图命令bitcountbitop

    7)增强了 redis-benchmark 的功能:支持 定制化的压测CSV输出 等功能。

    8)基于 浮点数 自增命令:incrbyfloathincrbyfloat

    9)redis-cli 可以使用 --eval 参数实现 Lua脚本执行

    10)shutdown 命令增强。

    11)info 可以按照 section 输出,并且添加了一些统计项。

    12)重构了大量的核心代码,所有 集群相关的代码 都去掉了,cluster 功能 将会是 3.0版本 最大的亮点

    13)sort 命令优化。

  2. Redis 2.8

    Redis 2.82013 年 11 月 22 日正式发布,经历了 24 个版本,到 2.8.24 版本,相比于 Redis 2.6,主要特性如下:

    1)添加部分 主从复制 的功能,在一定程度上 降低了 由于 网络问题造成频繁 全量复制生成RDB 对系统造成的压力。

    2)尝试性地支持 IPv6

    3)可以通过 config set 命令设置 maxclients

    4)可以用 bind 命令 绑定 多个 IP地址

    5)Redis 设置 了明显的 进程名,方便使用 ps 命令 查看系统进程。

    6)config rewrite 命令可以将 config set 持久化到 Redis配置文件 中。

    7)发布订阅 添加了 pubsub 命令。

    8)Redis Sentinel 第二版,相比于 Redis 2.6 的 Redis Sentinel,此版本已经变成 生产可用

  3. Redis 3.0

    Redis 3.02015 年 4 月 1 日 正式发布,截止到本书完成已经到 3.0.7 版本,相比于 Redis 2.8 主要特性如下:

    Redis 3.0 最大的改动就是添加 Redis的分布式 实现 Redis Cluster,填补了 Redis 官方没有 分布式 实现的空白。

    Redis Cluster 经历了 4 年才正式发布也是有原因的,具体可以参考 Redis Cluster 的开发日志(http://antirez.com/news/79)。

    1)Redis Cluster:Redis 的 官方 分布式实现

    2)全新的 embedded string 对象编码结果,优化 小对象 内存访问,在特定的工作负载下 速度大幅提升

    3)lur 算法大幅提升。

    4)migrate 连接缓存,大幅提升 键迁移的速度

    5)migrate 命令 两个新的参数 copyreplace

    6)新的 client pause 命令,在指定时间内停止处理客户端请求

    7)bitcount 命令性能提升。

    8)config set 设置 maxmemory 时候可以 设置不同的单位(之前只能是字节),例如 config set maxmemory 1gb

    9)Redis 日志小做调整:日志中会反应 当前实例的角色master 或者 slave)。

    10)incr 命令 性能提升

  4. Redis 3.2

    Redis 3.22016 年 5 月 6 日正式发布,相比于 Redis 3.0 主要特征如下:

    1)添加 GEO 相关功能。

    2)SDS速度节省空间 上都做了优化。

    3)支持用 upstart 或者 systemd 管理 Redis 进程

    4)新的 List 编码类型:quicklist

    5)从结点 读取 过期数据 保证一致性

    6)添加了 hstrlen 命令。

    7)增强了 debug 命令,支持了更多的参数。

    8)Lua 脚本功能增强。

    9)添加了 Lua Debugger

    10)config set 支持更多的配置参数。

    11)优化了 Redis 崩溃 后的相关报告。

    12)新的 RDB 格式,但是仍然兼容旧的 RDB。

    13)加速 RDB 的加载速度。

    14)spop 命令支持 个数 参数。

    15)cluster nodes 命令得到 加速。

    16)Jemalloc 更新到 4.0.3 版本。

  5. Redis 4.0

    可能出乎很多人的意料,Redis 3.2 之后的版本是 4.0,而不是 3.4、3.6、3.8。

    一般这种 重大版本号 的 升级 也意味着 软件或者工具本身发生了重大变革,直到本书截稿前,Redis 发布了 4.0-RC2,下面列出 Redis 4.0 的新特性:

    1)提供了 模块系统方便第三方开发者 拓展 Redis 的功能,更多模块详见:http://redismodules.com。

    2)PSYNC 2.0优化了之前版本中,主从节点切换 必然引起 全量复制 的问题

    3)提供了新的 缓存剔除 算法LFULast Frequently Used),并对已有算法进行了优化。

    4)提供了 非阻塞 delflushall/flushdb 功能,有效解决 删除 bigkey 可能造成的 Redis 阻塞

    5)提供了 RDB-AOF 混合持久式 格式,充分利用了 AOF 和 RDB 各自优势。

    6)提供 memory 命令,实现 对内存更为全面的监控统计

    7)提供了 交互数据库 功能,实现 Redis 内部数据库之间的 数据置换

    8)Redis Cluster 兼容 NATDocker

1.7 本章重点回顾

1)Redis8个特性速度快基于键值对的数据结构服务器功能丰富简单稳定客户端语言多、持久化主从复制支持高可用和分布式

2)Redis不是 万金油,有些场景不适合 Redis 进行开发。

3)开发运维结合 以及 阅读源码 是用好 Redis 的重要方法。

4)生产环境 中使用 配置文件 启动 Redis

5)生产环境 选取 稳定版本的 Redis

6)Redis 3.0 是重要的里程碑,发布了 Redis官方的 分布式实现 Redis Cluster

第2章 API的理解和使用

​ Redis 提供了 5种 数据结构,理解每种数据结构的特点 对于 Redis 开发运维 非常重要,同时掌握 Redis的单线程命令处理机制,会使 数据结构和命令的选择事半功倍,本章内容如下:

  • 预备知识:几个简单的全局命令数据结构内部编码单线程命令处理机制分析。
  • 5种 数据结构特点,命令使用应用场景
  • 键管理遍历键数据库管理
2.1 预备

​ 在正式介绍 5种数据结构 之前,了解一下 Redis 的一些 全局命令数据结构内部编码单线程命令处理机制 是十分有必要的,它们能为后面内容的学习打下一个好的基础,主要体现在两个方面:

第一、Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性

第二Redis不是万金油,有些数据结构 和 命令 必须在特定场景下使用,一旦使用不当 可能对 Redis本身 或者 应用本身造成致命伤害。

2.1.1 全局命令

Redis5种数据结构,它们是 键值对中的 值,对于 来说有一些 通用的命令

  1. 查看所有键

    keys *

    下面插入了 3对 字符串类型的键值对

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> set java jedis
    OK
    127.0.0.1:6379> set python redis-py
    OK
    

    keys * 命令会 将所有的键输出

    127.0.0.1:6379> keys *
    1) "python"
    2) "java"
    3) "hello"
    
  2. 键总数

    dbsize

    下面插入一个 列表类型的键值对(值 是多个元素组成)

    127.0.0.1:6379> rpush mylist a b c d e f g
    (integer) 7
    

    dbsize 命令会返回 当前数据库中键的总数

    例如 当前数据库有 4 个键,分别是 hello、java、python、mylist,所以 dbsize 的结果是 4:

    127.0.0.1:6379> dbsize
    (integer) 4
    

    dbsize 命令在 计算键总数 时 不会遍历所有键,而是 直接获取 Redis 内置的 键总数 变量,所以 dbsize 命令的 时间复杂度O(1)

    keys 命令会 遍历所有键,所以它的 时间复杂度O(n)当 Redis 保存了 大量键时,线上环境 禁止使用

  3. 检查键是否存在

    exists key

    如果 键存在 则返回1不存在则返回0

    127.0.0.1:6379> exists java
    (integer) 1
    127.0.0.1:6379> exists not_exist_key
    (integer) 0
    
  4. 删除键

    del key [key ...]

    del 是一个通用命令无论值是什么数据结构类型del 命令 都可以将其删除,例如下面将 字符串类型的键 java 和 列表类型的键 mylist 分别删除:

    127.0.0.1:6379> del java
    (integer) 1
    127.0.0.1:6379> exists java
    (integer) 0
    127.0.0.1:6379> del mylist
    (integer) 1
    127.0.0.1:6379> exists mylist
    (integer) 0
    

    返回结果为 成功删除键的个数假设删除一个不存在的键,就会返回0

    127.0.0.1:6379> del not_exist_key
    (integer) 0
    

    同时 del 命令可以支持 删除多个键

    127.0.0.1:6379> set a 1
    OK
    127.0.0.1:6379> set b 2
    OK
    127.0.0.1:6379> set c 3
    OK
    127.0.0.1:6379> del a b c
    (integer) 3
    
  5. 键过期

    expire key seconds

    Redis 支持对 键 添加过期时间当超过 过期时间 后,会自动删除 键,例如为 键 hello 设置了 10秒过期时间:

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> expire hello 10
    (integer) 1
    

    ttl 命令会返回 键的剩余过期时间,它有 3种返回值

    • 大于等于0 的整数键剩余的过期时间
    • -1键没设置过期时间
    • -2键不存在

    可以通过 ttl 命令观察键 hello 的剩余过期时间:

    # 还剩7秒
    127.0.0.1:6379> ttl hello
    (integer) 7
    ...
    # 还剩1秒
    127.0.0.1:6379> ttl hello
    (integer) 1
    # 返回结果为 -2 ,说明 键hello 已经被删除
    127.0.0.1:6379> ttl hello
    (integer) -2
    127.0.0.1:6379> get hello
    (nil)
    

    有关 键过期 更为详细的使用以及原理会在 2.7 节介绍。

    1. 键的数据结构类型

    type key

    例如键 hello 是字符串类型,返回结果为 string。

    键 mylist 是列表类型,返回结果为 list。

    127.0.0.1:6379> set a b
    OK
    127.0.0.1:6379> type a
    string
    127.0.0.1:6379> rpush mylist a b c d e f g
    (integer) 7
    127.0.0.1:6379> type mylist
    list
    

    如果 键不存在,则返回 none :

    127.0.0.1:6379> type not_exist_key
    none
    

    本小节只是抛砖引玉,给出几个通用的命令,为 5种数据结构的使用 做一个热身,2.7节 将对 键管理 做一个更为详细的介绍。

2.1.2 数据结构和内部编码

type 命令实际返回的就是 当前键的数据结构类型,它们分别是:string字符串)、hash哈希)、list列表)、set集合)、zset有序集合),但这些只是 Redis 对外的数据结构,如图 2-1 所示。

实际上 每种 数据结构 都有自己 底层的内部编码 实现,而且是 多种实现,这样 Redis 会在 合适的场景选择合适的内部编码,如图 2-2 所示。

可以看到 每种数据结构 都有 两种以上的内部编码实现,例如 list 数据结构包含了 linkedlistziplist 两种内部编码。同时 有些内部编码,例如 ziplist可以作为 多种外部数据结构 的 内部实现,可以通过 object encoding 命令 查询内部编码

127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"ziplist"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BB8LhGU3-1621301836862)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517120419269.png)]

可以看到 键hello 对应值的内部编码是 embstr,键 mylist 对应值的内部编码是 ziplist。

Redis 这样设计有两个好处

第一,可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplistlinkedlist 两者的优势,为 列表类型 提供了 一种更为优秀的内部编码实现,而对 外部用户 来说基本感知不到。

第二多种内部编码实现 可以在 不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在 列表元素 比较多 的情况下,性能会有所下降,这时候 Redis 会根据 配置选项列表类型的内部实现 转换为 linkedlist

数据结构stringhashlistsetzset
内部编码rawhashtablelinkedlisthashtableskiplist
intziplistziplistintsetziplist
embstr
2.1.3 单线程架构

Redis 使用了 单线程架构I/O多路复用模型 来实现 高性能内存数据库服务,本节首先通过 多个客户端命令调用 的例子说明 Redis 单线程命令处理机制,接着分析 Redis 单线程模型 为什么性能如此之,最终给出为什么 理解单线程模型 是 使用和运维 Redis 的关键。

  1. 引出单线程模型

    现在开启了 三个 redis-cli 客户端 同时执行命令、

    客户端1 设置一个 字符串键值对:

    127.0.0.1:6379> set hello world
    

    客户端2 对 counter 做自增操作:

    127.0.0.1:6379> incr counter
    

    客户端3 对 counter 做自增操作:

    127.0.0.1:6379> incr counter
    

    Redis 客户端 与 服务端 的模型可以简化成 图2-3,每次 客户端调用都经历了 发送命令执行命令返回结果 三个过程。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oAqoELf9-1621301836863)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517124231618.png)]

    其中 第2步 是重点要讨论的,因为 Redis 是 单线程 来处理命令的,所以 一条命令从客户端 到达 服务端 不会立刻被执行所有命令都会进入一个 队列 中然后 逐个被执行

    所以上面 3个客户端 命令的执行顺序是不确定的(如图2-4所示),但是可以确定 不会有两条命令被同时执行(如图2-5所示),所以 incr 命令无论怎么执行最终结果都是 2,不会产生并发问题,这就是 Redis 单线程的基本模型

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4uukxun5-1621301836864)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517124353808.png)]

    但是像 发送命令、返回结果、命令排队 肯定不像描述的这么简单,Redis 使用了 I/O多路复用技术 来解决 I/O 的问题,下一节将进行介绍。

  2. 为什么单线程还能这么快

    通常来讲,单线程处理能力 要比 多线程 差

    例如有 10 000 斤货物,每辆车的运载能力是每次 200斤,那么要50次才能完成,

    但是如果有 50辆车,只要安排合理,只需一次就可以完成任务。

    那么为什么 Redis 使用 单线程模型 会达到每秒万级别的处理能力 呢?

    可以将其归结为三点

    第一纯内存访问,Redis 将 所有数据放在 内存 中内存 的 响应时长 大约为 100纳秒,这是 Redis 达到每秒 万级别 访问的重要基础

    第二非阻塞I/O,Redis 使用 epoll 作为 I/O 多路复用技术 的 实现,再加上 Redis 自身的 事件处理模型epoll 中的 连接、读写、关闭 都转换为 事件不在 网络 I/O 上浪费过多的时间,如图 2-6 所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8LbJKfu9-1621301836865)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517124935714.png)]

    第三单线程 避免了 线程切换 和 竞态产生 的消耗

    既然采用 单线程 就能达到如此高的性能,那么也不失为一种不错的选择,

    因此单线程能带来几个好处

    第一单线程 可以 简化 数据结构和算法 的实现。如果对高级编程语言熟悉的读者应该了解 并发数据结构实现 不但困难 而且 开发测试比较麻烦。

    第二单线程 避免了 线程切换 和 竞态产生 的消耗,对于 服务端开发 来说,锁 和 线程切换 通常是 性能杀手

    但是 单线程 会有一个问题:对于 每个命令的执行时间 是有要求的。如果某个命令 执行过长,会造成 其他命令的 阻塞,对于 Redis 这种 高性能的服务 来说是致命的,所以 Redis 是面向 快速执行 场景的数据库

    单线程机制 很容易被初学者忽视,但笔者认为 Redis 单线程机制 是 开发和运维人员 使用和理解 Redis 的核心之一,随着后面的学习,相信读者会逐步理解。

2.2 字符串

字符串类型 是 Redis 最基础的数据结构

首先 都是 字符串类型,而且 其他几种数据结构 都是在 字符串类型 基础上构建的,所以 字符串类型 能为 其他四种数据结构的学习 奠定基础。

如图 2-7 所示,字符串类型的值 实际可以是 字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是 值最大 不能超过 512MB

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zYS8Dm8l-1621301836866)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517130333277.png)]

2.2.1 命令

​ 字符串类型 的 命令 比较多,本小节 将按照 常用和不常用 两个维度 进行说明,但是这里 常用和不常用 是相对的,希望读者尽可能都去了解和掌握。

  1. 常用命令

    (1)设置值

    set key value [ex seconds] [px milliseconds] [nx|xx]
    

    下面操作 设置键为hello,值为world的键值对,返回结果为 OK 代表设置成功:

    127.0.0.1:6379> set hello world
    OK
    

    set 命令 有几个选项:

    • ex seconds为 键 设置 秒级 过期时间
    • px milliseconds为 键 设置 毫秒级 过期时间
    • nx键 必须不存在,才可以设置成功,用于添加
    • xx与 nx 相反,键必须存在,才可以设置成功,用于更新

    除了 set 选项,Redis 还提供了 setexsetnx 两个命令:

    setex key seconds value
    setnx key value
    

    它们的作用和 exnx 选项是一样的。

    下面的例子说明了 setsetnxset xx 的区别。

    当前 键 hello 不存在:

    127.0.0.1:6379> exists hello
    (integer) 0
    

    设置 键为hello, 值为world 的键值对:

    127.0.0.1:6379> set hello world
    OK
    

    因为键 hello 已存在,所以 setnx 失败,返回结果为 0

    127.0.0.1:6379> setnx hello world
    (integer) 0
    

    因为键 hello 已存在,所以 set xx 成功,返回结果为 OK。

    127.0.0.1:6379> set hello jedis xx
    OK
    

    setnxsetxx 在实际使用中有什么应用场景吗?

    setnx 命令为例子,由于 Redis 的 单线程命令处理机制,如果有 多个客户端 同时执行 setnx key value,根据 setnx 的特性 只有一个客户端能设置成功setnx 可以作为 分布式锁 的一种实现方案,Redis 官方给出了使用 setnx 实现分布式锁 的方法:http://redis.io/topics/distlock。

    (2)获取值

    get key
    

    下面操作 获取键hello的值:

    127.0.0.1:6379> get hello
    "world"
    

    如果 要获取的键 不存在,则返回 nil(空)

    127.0.0.1:6379> get not_exist_key
    (nil)
    

    (3)批量设置值

    mset key value [key value ...]
    

    下面操作通过 mset 命令一次性设置 4个键值对:

    127.0.0.1:6379> mset a 1 b 2 c 3 d 4
    OK
    

    (4)批量获取值

    mget key [key ...]
    

    下面操作批量获取了键 a、b、c、d的值:

    127.0.0.1:6379> mget a b c d
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    

    如果有些 键 不存在,那么它的值 为 nil(空),结果是按照 传入键 的顺序 返回

    127.0.0.1:6379> mget a b c f
    1) "1"
    2) "2"
    3) "3"
    4) (nil)
    

    批量操作 命令 可以 有效提高开发效率,假如没有 mget 这样的命令,要执行 n 次 get 命令 需要按照图 2-8 的方式来执行,具体耗时如下:

    n 次 get 时间 = n 次网络时间 + n 次命令时间
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tpk8mJqK-1621301836867)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517133127643.png)]

    使用 mget 命令后,要执行 n 次 get 命令操作 只需要按照图 2-9 的方式来完成,具体耗时如下:

    n 次 get 时间 = 1 次网络时间 + n 次命令时间
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KrQD9scU-1621301836868)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517133254794.png)]

    Redis 可以支撑 每秒数万 的读写操作,但是这指的是 Redis 服务端的处理能力

    对于客户端来说,一次命令除了 命令时间 还有 网络时间,假设 网络时间为 1 毫秒,命令时间为 0.1 毫秒(按照每秒处理 1 万条命令算),那么执行 1000次 get 命令 和 1次 mget 命令的区别 如表 2-1,因为 Redis 的处理能力已经足够高,对于开发人员来说,网络 可能会成为 性能 的瓶颈

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7z4vsnzX-1621301836869)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517133726771.png)]

    操作时间
    1 000 次 get1 000 × 1 + 1 000 × 0.1 = 1 100 毫秒 = 1.1 秒
    1 次 mget(组装了 1 000个键值对)1 × 1 + 1 000 × 0.1 = 101 毫秒 = 0.101 秒

    学会使用 批量操作,有助于 提高业务处理效率,但是要注意的是 每次批量操作 所发送的命令数 不是无节制的,如果数量过多 可能造成 Redis 阻塞 或者 网络拥塞

    (5)计数

    incr key
    

    incr 命令用于对值 做自增操作,返回结果分为三种情况:

    • 值不是整数,返回错误
    • 值是整数,返回自增后的结果
    • 键不存在,按照值为0 自增,返回结果为1

    例如对一个 不存在的键 执行 incr 操作后,返回结果是1:

    127.0.0.1:6379> exists key
    (integer) 0
    127.0.0.1:6379> incr key
    (integer) 1
    

    再次对键 执行 incr 命令,返回结果是 2:

    127.0.0.1:6379> incr key
    (integer) 2
    

    如果值 不是整数,那么会 返回错误。

    127.0.0.1:6379> set hello world
    OK
    127.0.0.1:6379> incr hello
    (error) ERR value is not an integer or out of range
    

    除了 incr 命令,Redis 提供了 decr自减)、incrby自增指定数字)、decrby自减指定数字)、incrbyfloat自增浮点数):

    decr key
    incrby key increment
    decrby key decrement
    incrbyfloat key increment
    

    很多 存储系统和编程语言内部 使用 CAS机制实现计数功能会有一定的CPU开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构任何命令到了 Redis 服务端 都要 顺序执行

  2. 不常用命令

    (1)追加值

    append key value
    

    append 可以向 字符串尾部 追加值,例如:

    127.0.0.1:6379> get key
    "redis"
    127.0.0.1:6379> append key world
    (integer) 10
    127.0.0.1:6379> get key
    "redisworld"
    

    (2)字符串长度

    strlen key
    

    例如,当前值为 redisworld,所以返回值为10:

    127.0.0.1:6379> get key
    "redisworld"
    127.0.0.1:6379> strlen key
    (integer) 10
    

    下面操作返回结果为 6,因为 每个中文占用 3个字节:

    127.0.0.1:6379> set hello "世界"
    OK
    127.0.0.1:6379> strlen hello
    (integer) 6
    

    (3)设置并返回原值

    getset key value
    

    getsetset 一样会 设置值,但是不同的是,它同时会 返回键原来的值,例如:

    127.0.0.1:6379> getset hello world
    (nil)
    127.0.0.1:6379> getset hello redis
    "world"
    

    (4)设置 指定位置 的 字符

    setrange key offeset value
    

    下面操作将由 pest 变为了 best:

    127.0.0.1:6379> set redis pest
    OK
    127.0.0.1:6379> setrange redis 0 b
    (integer) 4
    127.0.0.1:6379> get redis
    "best"
    

    (5)获取 部分字符串

    getrange key start end
    

    startend 分别是 开始 和 结束的 偏移量([start, end]),偏移量 从 0 开始计算,例如下面操作获取了 值 best 的 前两个字符。

    127.0.0.1:6379> getrange redis 0 1
    "be"
    

    表 2-2 是 字符串类型命令的 时间复杂度,开发人员可以参考此表,结合自身业务需求 和 数据大小 选择合适的命令。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bA8PTUZ3-1621301836874)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517141143590.png)]

    命令时间复杂度
    set key valueO(1)
    get keyO(1)
    del key [key …]O(k),k 是键的个数
    mset key value [key value …]O(k),k 是键的个数
    mget key [key …]O(k),k 是键的个数
    incr keyO(1)
    decr keyO(1)
    incrby key incrementO(1)
    decrby key decrementO(1)
    incrbyfloat key incrementO(1)
    append key valueO(1)
    strlen keyO(1)
    setrange key offset valueO(1)
    getrange key start endO(n),n是字符串长度,由于获取字符串非常快,所以如果字符串不是很长,可以视同为O(1)
2.2.2 内部编码

字符串类型的 内部编码 有3种

  • int8个字节的长整型
  • embstr小于等于39个字节的字符串
  • raw大于39个字节的字符串

Redis 会根据 当前值的 类型 和 长度 决定使用哪种内部编码 实现

整数类型示例如下:

127.0.0.1:6379> set key 8653
OK
127.0.0.1:6379> object encoding key
"int"

短字符串示例如下:

# 小于等于 39 个字节 的字符串:embstr
127.0.0.1:6379> set key "hello world"
OK
127.0.0.1:6379> object encoding key
"embstr"

长字符串示例如下:

# 大于 39个字节 的字符串:raw
127.0.0.1:6379> set key "one string greater than 39 byte........."
OK
127.0.0.1:6379> object encoding key
"raw"
127.0.0.1:6379> strlen key
(integer) 40

有关 字符串类型的内存优化技巧 将在 8.3 节详细介绍。

2.2.3 典型使用场景
  1. 缓存功能

    图 2-10 是比较典型的 缓存使用场景,其中 Redis 作为 缓存层MySQL 作为 存储层绝大部分请求的数据都是从 Redis 中获取

    由于 Redis 具有支撑 高并发 的特性,所以 缓存 通常能起到 加速读写降低后端压力 的作用。

    下面伪代码模拟了 图 2-10 的访问过程:

    1) 该函数用于 获取用户的基础信息:

    UserInfo getUserInfo(long id){
        ...
    }
    

    2)首先从 Redis 获取用户信息:

    //定义键
    userRedisKey = "user:info:"+id;
    
    //从 Redis 获取值
    value = redis.get(userRedisKey);
    if(value != null){
        //将值 进行反序列化 为UserInfo 并返回结果
        userInfo = deserialize(value);
        return UserInfo;
    }
    

    开发提示

    与 MySQL 等关系型数据库不同的是,Redis 没有 命令空间,而且也没有对 键名 有强制要求(除了 不能使用一些特殊字符)。

    设计合理的键名,有利于 防止键冲突项目的可维护性,比较推荐的方式是使用 “业务名 : 对象名 : id : [ 属性 ]” 作为 键名(也可以不是分号)。

    例如 MySQL 的 数据库名为 vs,用户表名为 user,那么对应的键可以用 “vs:user:1”,“vs:user:1:name"来表示,例如"user : {uid} : friends : messages : {mid}”,可以在 能描述键含义的前提下 适当减少键的长度,例如变为 “u : {uid} : fr : m : {mid}”,从而 减少由于 键过长的内存浪费

    3)如果没有从 Redis 获取到用户信息,需要从 MySQL 中进行获取,并将结果回写到 Redis,添加 1小时(3600秒)过期时间:

    //从 MySQL 获取用户信息
    userInfo = mysql.get(id);
    
    //将 userInfo 序列化,并存入 redis
    redis.setex(userRedisKey, 3600, serialize(userInfo));
    
    //返回结果
    return userInfo;
    

    整个功能的伪代码如下:

    UserInfo getUserInfo(long id){
        userRedisKey = "user:info:"+id;
        value = redis.get(userRedisKey);
        UserInfo userInfo;
        if(value != null){
            userInfo = deserialize(value);
        }else{
            userInfo = mysql.get(id);
            if(userInfo != null)
                	redis.setex(userRedisKey, 3600, serialize(userInfo));
        }
        return userInfo;
    }
    
  2. 计数

    许多应用都会使用 Redis 作为计数的 基础工具,它可以实现 快速计数查询缓存的功能,同时 数据 可以 异步落地 到 其他数据源

    例如笔者所在团队的 视频播放数系统 就是使用 Redis 作为视频播放数 计数的基础组件,用户每播放一次视频,相应的视频播放数 就会 自增1。

    long incrVideoCounter(long id){
        key = "Video:playCount:"+id;
        return redis.incr(key);
    }
    

    开发提示

    实际上一个真实的 计数系统 要考虑的问题会很多:防作弊按照不同维度计数数据持久化到底层数据源等。

  3. 共享Session

    如图 2-11 所示,一个 分布式 Web 服务 将用户的 Session信息 (例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务 会将 用户的访问 均衡到不同服务器上用户刷新一次访问 可能会发现 需要重新登录,这个问题是 用户无法容忍的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cRZQHxM6-1621301836877)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517150020342.png)]

    为了解决这个问题,可以使用 Redis 将用户的Session进行集中管理,如图 2-12 所示,在这种模式下 只要保证 Redis是 高可用 和 扩展性 的,每次用户更新或者查询登录信息 都是直接从 Redis 中集中获取。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jG7RsGBG-1621301836878)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517150425297.png)]

  4. 限速

    很多应用处于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。

    但是为了 短信接口 不被频繁访问会限制 用户每分钟获取验证码的频率,例如一分钟不能超过 5次,如图 2-13 所示。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZEaLrmw9-1621301836879)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517150617130.png)]

    此功能可以使用 Redis 来实现,下面的伪代码给出了基本实现思路:

    phoneNum = "138xxxxxxxx";
    key = "shortMsg:limit:"+phoneNum;
    // SET key value EX 60 NX
    isExists = redis.set(key, 1, "EX 60", "NX");
    if(isExists != null || redis.incr(key) <=5){
        //通过
    }else{
        //限速
    }
    

    上述就是利用 Redis 实现了 限速功能,例如一些网站限制一个 IP地址 不能在一秒钟之内访问超过 n次 也可以采用类似的思路。

    除了上面介绍的几种使用场景,字符串还有非常多的使用场景,开发人员可以结合字符串提供的相应命令充分发挥自己的想象力。

2.3 哈希

​ 几乎所有的编程语言都提供了 哈希hash)类型,它们的叫法可能是 哈希字典关联数组

Redis 中,哈希类型是指 键本身 又是一个键值对结构,形如 value={{field1,value1}, ...{fieldN,valueN}},Redis键值对 和 哈希类型 两者的关系 可以用图 2-14来表示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xJws3o4J-1621301836879)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517151327641.png)]

注意

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

注意这里的 value 是指 field 对应的值,

不是 键对应的值,请注意 value 在不同上下文的作用。

2.3.1 命令

(1)设置值

hset key field value

下面为 user:1 添加一对 field-value:

127.0.0.1:6379> hset user:1 name tom
(integer) 1

如果 设置成功 会返回1,反之会返回0

此外 Redis 提供了 hsetnx 命令,它们的关系就像 setsetnx 命令一样,只不过作用域 由 变为 field

(2)获取值

hget key field

例如,下面操作获取 user:1 的 name 域(属性)对应的值:

127.0.0.1:6379> hget user:1 name
"tom"

如果 键 或 field 不存在,会返回 nil

127.0.0.1:6379> hget user:2 name
(nil)
127.0.0.1:6379> hget user:1 age
(nil)

(3)删除 field

hdel key field [field ...]

hdel 会删除 一个 或 多个 field,返回结果为 成功删除 field 的个数,例如:

127.0.0.1:6379> hdel user:1 name
(integer) 1
127.0.0.1:6379> hdel user:1 age
(integer) 0

(4)计算 field 个数

hlen key

例如 user:1 有 3 个field:

127.0.0.1:6379> hset user:1 name tom
(integer) 1
127.0.0.1:6379> hset user:1 age 23
(integer) 1
127.0.0.1:6379> hset user:1 city tianjin
(integer) 1
127.0.0.1:6379> hlen user:1
(integer) 3

(5)批量设置 或 获取 field-value

hmget key field [field ...]
hmset key field value [field value ...]

hmsethmget 分别是 批量设置 和 获取 field-valuehmset 需要的参数是 key多对 field-valuehmget 需要的参数是 key多个 field

例如:

127.0.0.1:6379> hmset user:1 name mike age 12 city tianjin
OK
127.0.0.1:6379> hmget user:1 name city
1) "mike"
2) "tianjin"

(6)判断 field 是否存在

hexists key field

例如,user:1 包含 name 域,所以 返回结果为 1,不包含时返回0

127.0.0.1:6379> hexists user:1 name
(integer) 1

(7)获取所有field

hkeys key

hkeys 命令应该叫 hfields 更为恰当,它返回 指定哈希键 所有的field,例如:

127.0.0.1:6379> hkeys user:1
1) "name"
2) "age"
3) "city"

(8)获取所有 value

hvals key

下面操作 获取 user:1 全部 value:

127.0.0.1:6379> hvals user:1
1) "mike"
2) "12"
3) "tianjin"

(9)获取所有的field-value

hgetall key

下面操作获取 user:1 所有的field-value

127.0.0.1:6379> hgetall user:1
1) "name"
2) "mike"
3) "age"
4) "12"
5) "city"
6) "tianjin"

开发提示

在使用 hgetall 时,如果 哈希元素个数比较多,会存在阻塞 Redis 的可能

如果开发人员 只需要获取部分field,可以使用 hmget,如果 一定要获取 全部 field-value,可以使用 hscan 命令,该命令会 渐进式遍历哈希类型hscan 将在 2.7节 介绍。

(10) hincrby hincrbyfloat

hincrby key field
hincrbyfloat key field

hincrbyhincrbyfloat,就像 incrbyincrbyfloat 命令一样,但是它们的 作用域field

(11)计算 value 的字符串长度(需要 Redis 3.2 以上)

hstrlen key field

例如 hget user:1 namevaluetom,那么 hstrlen 的返回结果是 3:

127.0.0.1:6379> hstrlen user:1 name
(integer) 3

表 2-3 是 哈希类型命令的时间复杂度,开发人员可以参考此表选择合适的命令。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rY0t7IhA-1621301836880)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517163340964.png)]

命令时间复杂度
hset key field valueO(1)
hget key fieldO(1)
hdel key field [field …]O(k),k 是field个数
hlen keyO(1)
hgetall keyO(n),n 是field总数
hmget key field [field …]O(k),k 是field的个数
hmset key field value [field value …]O(k),k 是field的个数
hexists key fieldO(1)
hkeys keyO(n),n 是field总数
hvals keyO(n),n 是field 总数
hsetnx key field valueO(1)
hincrby key field incrementO(1)
hincrbyfloat key field incrementO(1)
hstrlen key fieldO(1)
2.3.2 内部编码

哈希类型内部编码两种

  • ziplist压缩列表

    • 哈希类型元素个数 小于 hash-max-ziplist-entries 配置(默认 512 个)、同时 所有 值 都小于 hash-max-ziplist-value 配置(默认 64字节)时,Redis 会使用 ziplist 作为 哈希的 内部实现ziplist 使用更加 紧凑的结构 实现 多个元素的连续存储,所以在 节省内存方面hashtable 更加优秀。
  • hashtable哈希表

    • 哈希类型 无法满足 ziplist 的条件 时,Redis 会使用 hashtable 作为 哈希的内部实现,因为此时 ziplist读写效率 会下降,而 hashtable读写时间复杂度为O(1)

    下面的示例演示了 哈希类型的内部编码,以及相应的变化。

    1)当 field 个数比较少 且 没有大的 value 时,内部编码ziplist

    127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
    OK
    127.0.0.1:6379> object encoding hashkey
    "ziplist"
    

    2.1)当有 value 大于 64 字节内部编码会由 ziplist 变为 hashtable

    127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64bute...忽略..."
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"
    

    2.2)当 field 个数超过 512内部编码也会由 ziplist 变为 hashtable

    127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ...忽略... f513 v513
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"
    

    有关 哈希类型的内存优化技巧 将在8.3节中详细介绍。

2.3.3 使用场景

图 2-15 为 关系型数据表记录 的两条用户信息,用户的属性 作为 表的列,每条用户信息 作为 行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2soFSmP-1621301836881)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517171153802.png)]

idnameagecity
1tom23beijing
2mike30tianjin

如果将其用 哈希类型 存储,如图 2-16所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LFYe6u71-1621301836881)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517171236539.png)]

相比于使用 字符串序列化 缓存 用户信息哈希类型 变得更加直观,并且在 更新操作上 会更加便捷

可以将 每个用户的 id 定义为 键后缀,多对 field-value 对应 每个用户的属性,类似如下伪代码:

UserInfo getUserInfo(long id){
	//用户id 作为 key 后缀
	userRedisKey = "user:info:" + id;
	
	//使用 hgetall 获取所有用户信息映射关系
	userInfoMap = redis.hgetall(userRedisKey);
	UserInfo userInfo;
	if (userInfoMap != null){
        //将映射关系转换为 UserInfo
        userInfo = transferMapToUserInfo(userInfoMap);
    }else{
        // 从 MySQL 中获取 用户信息
        userInfo = mysql.get(id);
        
        //将UserInfo 变为映射关系 使用 hmset 保存到 Redis 中
        redis.hmset(userRedisKey,transferUserInfoToMap(userInfo));
        
        //添加过期时间
        redis.expire(userRedisKey, 3600);
    }
    return userInfo;
}

但是需要注意的是 哈希类型关系型数据库两点不同之处:

  • 哈希类型稀疏的,而 关系型数据库 是完全 结构化的,例如 哈希类型 每个键 可以有不同的 field,而 关系型数据库一旦 添加新的列,所有行 都要为其 设置值(即使为NULL),如图2-17所示。
  • 关系型数据库 可以做 复杂的关系查询,而 Redis 去模拟关系型复杂查询 开发困难,维护成本高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6IYVNvHA-1621301836882)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517172939955.png)]

开发人员需要将两者的特点搞清楚,才能在合适的场景使用合适的技术。

到目前为止,我们已经能够 用三种方法缓存用户信息,下面给出 三种方案的实现方法和优缺点分析。

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 tom age 23 city beijing 

优点:简单直观,如果 使用合理 可以减少 内存空间的使用

缺点:要控制 哈希在 ziplisthashtable 两种内部编码的转换hashtable消耗更多内存

2.4 列表

列表list)类型 是用来 存储多个 有序的 字符串

如图 2-18 所示,a、b、c、d、e 五个元素 从左到右 组成了一个 有序的列表,列表中的 每个字符串 称为 元素element),

一个列表 最多可以存储 2的32次方-1 个元素。

在 Redis 中,可以对 列表 两端 插入push)和 弹出pop),还可以获取 指定范围的元素列表、获取 指定索引下标的元素 等(如图 2-18 和 图 2-19 所示)。

列表 是一种 比较灵活的 数据结构,它可以充当 队列 的角色,在实际开发上 有很多应用场景。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NI5bMq4X-1621301872080)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517205903000.png)]

列表类型两个特点

第一列表 中的 元素 是 有序的,这就意味着可以通过 索引下标 获取某个元素 或者 某个范围内的 元素列表,例如要获取 图 2-19的 第5个元素,可以执行 lindex user:1:message 4索引从 0 算起)就可以得到元素e。

第二列表 中的 元素 可以是 重复的,例如图 2-20所示 列表中 包含了 两个字符串 a。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Wnl6pK9-1621301872082)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517210257862.png)]

这两个特点在后面介绍 集合有序集合 后,会显得更加突出,因此在考虑是否使用该数据结构前,首先需要弄清楚 列表 数据结构 的特点。

2.4.1 命令

下面将按照对 列表的 5种操作类型 对命令 进行介绍,命令如表2-4所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PLEeBqM9-1621301872085)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517210707928.png)]

操作类型操作
添加rpush lpush linsert
lrange lindex llen
删除lpop rpop lrem ltrim
修改lset
阻塞操作blpop brpop
  1. 添加操作

    (1)从右边插入元素

    rpush key value [value ...]
    

    下面代码 从右向左 插入元素 c、b、a:

    127.0.0.1:6379> rpush listkey c b a
    (integer) 3
    

    lrange 0 -1 命令可以获取 从左到右 获取列表的所有元素

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "b"
    3) "a"
    

    (2)从左边插入元素

    lpush key value [value ...]
    

    使用方法和 rpush 相同,只不过从左侧插入,这里不再赘述。

    (3)向 某个元素 前 或者 后 插入元素

    linsert key before|after pivot value
    

    linsert 命令会从 列表 中 找到等于 pivot元素,在其 before) 或者 after插入一个新的元素 value,例如下面操作会在列表的元素 b 前 插入 java:

    127.0.0.1:6379> linsert listkey before b java
    (integer) 4
    

    返回结果为 4,代表 当前列表的长度,当前列表变为:

    127.0.0.1:6379> lrange listkey 0 -1
    1) "c"
    2) "java"
    3) "b"
    4) "a"
    
  2. 查找

    (1) 获取 指定范围内的元素列表

    lrange key start end
    

    lrange 操作会 获取列表 指定索引范围 所有的元素

    索引下标两个特点

    第一索引下标 从左到右 分别是 0 到 N-1,但是 从右到左 分别是 -1 到 -N

    第二lrange 中的 end 选项包含了自身,这个和很多编程语言不包含 end 不太相同,例如想获取 列表的第2到第4个元素,可以执行如下操作:

    127.0.0.1:6379> lrange listkey 1 3
    1) "java"
    2) "b"
    3) "a"
    

    (2)获取 列表 指定索引下标 的元素

    lindex key index
    

    例如 当前列表 最后一个元素为a:

    127.0.0.1:6379> lindex listkey -1
    "a"
    

    (3) 获取列表长度

    llen key
    

    例如,下面示例 当前列表长度为 4:

    127.0.0.1:6379> llen listkey
    (integer) 4
    
  3. 删除

    (1)从列表左侧 弹出 元素

    lpop key
    

    如下操作将 列表最左侧的元素 c 会被弹出,弹出后列表变为 java、b、a:

    127.0.0.1:6379> lpop listkey
    "c"
    127.0.0.1:6379> lrange listkey 0 -1
    1) "java"
    2) "b"
    3) "a"
    

    (2)从列表右侧弹出

    rpop key
    

    它的使用方法和 lpop 是一样的,只不过从 列表 右侧弹出,这里不再赘述。

    (3)删除指定元素

    lrem key count value
    

    lrem 命令会从 列表中找到等于 value 的元素进行删除,根据 count 的不同 分为 三种情况

    • count > 0从左到右删除最多 count 个元素
    • count < 0从右到左删除最多 count 绝对值个元素
    • count = 0删除所有

    例如向 列表 从左向右 插入 5个a,那么当前列表变为 “a a a a a java b a”,下面操作将从 列表左边开始删除4个为a 的元素:

    127.0.0.1:6379> lrem listkey 4 a
    (integer) 4
    127.0.0.1:6379> lrange listkey 0 -1
    1) "a"
    2) "java"
    3) "b"
    4) "a"
    

    (4)按照索引范围 修剪 列表

    ltrim key start end
    

    例如,下面操作会 只保留 列表 listkey 第2个到第4个元素:

    127.0.0.1:6379> ltrim listkey 1 3
    OK
    127.0.0.1:6379> lrange listkey 0 -1
    1) "java"
    2) "b"
    3) "a"
    
  4. 修改

    修改指定索引下标的元素

    lset key index newValue
    

    下面操作会将 列表 listkey 中的 第3个 元素设置为 python:

    127.0.0.1:6379> lset listkey 2 python
    OK
    127.0.0.1:6379> lrange listkey 0 -1
    1) "java"
    2) "b"
    3) "python"
    
  5. 阻塞操作

    阻塞式弹出 如下:

    blpop key [key ...] timeout
    brpop key [key ...] timeout
    

    blpopbrpoplpoprpop阻塞版本,它们除了 弹出方向不同使用方法基本相同,所以下面以 brpop 命令进行说明,brpop 命令包含 两个参数:

    • key [key …]多个列表的键
    • timeout阻塞时间(单位:

    1)列表为空

    如果 timeout = 3,那么 客户端 要等到 3 秒后返回

    如果 timeout = 0,那么 客户端 一直阻塞等下去

    127.0.0.1:6379> brpop list:test 3
    (nil)
    (3.10s)
    127.0.0.1:6379> brpop list:test 0
    ...阻塞...
    

    如果此期间添加了数据 element1,客户端立即返回:

    127.0.0.1:6379> brpop list:test 3
    1) "list:test"
    2) "element1"
    (2.06s)
    

    2)列表不为空客户端会立即返回

    127.0.0.1:6379> brpop list:test 0
    1) "list:test"
    2) "element1"
    

    在使用 brpop 时,有 两点 需要注意。

    第一点,如果是 多个键,那么 brpop从左至右 遍历键一旦有一个键 能弹出元素,客户端立即返回

    127.0.0.1:6379> brpop list:1 list:2 list:3
    ...阻塞...
    

    此时 另一个客户端 分别向 list:2 和 list:3 插入元素:

    client-lpush> lpush list:2 element2
    (integer) 1
    client-lpush> lpush list:3 element3
    (integer) 1
    

    客户端会 立即返回 list:2 中的element2 ,因为 list:2 最先有可以弹出的元素:

    127.0.0.1:6379> brpop list:1 list:2 list:3
    1) "list:2"
    2) "element2"
    

    第二点,如果 多个客户端 对 同一个键执行 brpop,那么 最先执行 brpop 命令的客户端 可以获取到 弹出的值

    客户端1:

    client-1> brpop list:test 0
    ...阻塞...
    

    客户端2:

    client-2> brpop list:test 0
    ...阻塞...
    

    客户端3:

    client-3> brpop list:test 0
    ...阻塞...
    

    此时另一个客户端 lpush 一个元素到 list:test 列表中:

    client-lpush> lpush list:test element
    (integer) 1
    

    那么 客户端1 最先会获取到元素,因为 客户端1 最先执行 brpop,而客户端2 和 客户端3继续阻塞:

    client> brpop list:test 0
    1) "list:test"
    2) "element"
    

    有关 列表 的 基础命令 已经介绍完了,表 2-5 是这些命令的时间复杂度,开发人员可以参考此表 选择合适的命令。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55GHVUyL-1621301872089)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517214858071.png)]

    操作类型命令时间复杂度
    添加rpush key value [value …]O(k),k 是元素个数
    添加lpush key value [value …]O(k),k 是元素个数
    添加linsert key before|after pivot valueO(n),n 是pivot 距离列表头或尾的举例
    查找lrange key start endO(s+n),s 是 start 偏移量,n 是 start 到 end 的范围
    查找lindex key indexO(n),n 是索引的偏移量
    查找llen keyO(1)
    删除lpop keyO(1)
    删除rpop keyO(1)
    删除lrem count keyO(n),n 是列表长度
    删除ltrim key start endO(n),n 是要裁剪的元素总数
    修改lset key index valueO(n),n 是索引的偏移量
    阻塞操作blpop brpopO(1)
2.4.2 内部编码

列表类型内部编码两种

  • ziplist压缩列表

    • 列表的 元素个数 小于 list-max-ziplist-entries 配置(默认 512 个),同时 列表中 每个元素的值 都小于 list-max-ziplist-value 配置时(默认 64 字节),Redis 会选用 ziplist 来作为 列表的内部实现减少内存的使用
  • linkedlist链表

    • 列表类型 无法满足 ziplist条件时,Redis 会使用 linkedlist 作为 列表的内部实现

    下面的示例演示了 列表类型的内部编码,以及相应的变化。

    1)当 元素个数 较少 且 没有大元素 时,内部编码ziplist

    127.0.0.1:6379> rpush listkey e1 e2 e3
    (integer) 3
    127.0.0.1:6379> object encoding listkey
    "ziplist"
    

    2.1)当 元素个数超过 512个内部编码变为 linkedlist

    127.0.0.1:6379> rpush listkey e4 e5 ...忽略... e512 e 513
    (integer) 513
    127.0.0.1:6379> object encoding listkey
    "linkedlist"
    

    2.2)或者当 某个元素超过 64 字节内部编码也会变为 linkedlist

    127.0.0.1:6379> rpush listkey "one string is bigger than 64 byte ......................................."
    (integer) 4
    127.0.0.1:6379> object encoding listkey
    "linkedlist"
    

    开发提示

    Redis 3.2 版本提供了 quicklist 内部编码,

    简单地说它是 以一个ziplist 为节点的 linkedlist,它结合了 ziplistlinkedlist 两者的优势,为 列表类型 提供了一种更为优秀的内部编码实现,它的设计原理可以参考Redis的另一个作者Matt Stancliff 的博客:http://matt.sh/redis-quicklist。

    有关 列表类型的优化技巧 将在 8.3节 详细介绍。

2.4.3 使用场景
  1. 消息队列

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kzgBPzc3-1621301872090)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210517222037844.png)]

  2. 文章列表

    每个用户有属于自己的 文章列表,现需要 分页展示 文章列表。

    此时可以考虑使用 列表,因为 列表 不但是 有序的,同时 支持按照 索引范围 获取元素。

    1)每篇文章使用 哈希结构 存储,例如每篇文章有 3个属性 title、timestamp、content:

    hmset article:1 title xx timestamp 1476536196 content xxxx
    ...
    hmset article:k title yy timestamp 1476512536 content yyyy
    ....
    

    2)向 用户文章列表 添加文章,user : {id} : articles 作为 用户文章列表 的键:

    lpush user:1:articles article:1 article3
    ...
    lpush user:k:articles article:5
    ...
    

    3)分页 获取 用户文章列表,例如下面伪代码获取用户 id=1 的前 10 篇文章:

    articles = lrange user:1:articles 0 9
    for article in {articles}
    	hgetall {article}
    

    使用 列表类型保存 和 获取文章列表 会存在两个问题。

    第一,如果 每次分页获取的文章个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 Pipeline(第3章会介绍)批量获取,或者考虑 将文章数据序列化为字符串,使用 mget 批量获取。

    第二,分页获取文章列表时,lrange 命令在列表两端性能较好,但是如果 列表较大,获取列表中间范围的元素 性能会变差,此时可以考虑将 列表做二级拆分,或者使用 Redis 3.2 的quicklist 内部编码实现,它结合 ziplist 和 linkedlist 的特点,获取 列表中间范围的元素 时也可以高效完成。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值