Redis 6 学习笔记,课程源自尚硅谷,笔记持续更新中...

该笔记原先是基于尚硅谷的视频教学做的,后来找到一本书《Redis开发与运维》,由付磊和张益军两位大佬编制,大部分内容来自于这本书籍
尚硅谷视频教程链接

redis 6

简介

解决性能问题:Nosql、java线程、Hadoop、nginx、MQ、ES。

Nosql:不遵循sql标准、不支持ACID、远超于Sql的性能。

适用场景:对数据高并发的读写、海量数据读写、对数据高可扩展性。

redis:数据都在内存中,支持持久化,还支持多种数据类型,不只是简单的key-value,还支持list、set、hash、zset。一般作为缓存数据库辅助持久化数据库。


Redis安装

安装

一、首先上传reids的包,放到opt目录下,我是虚拟机桌面已经有了文件,再通过移动到opt下
在这里插入图片描述
二、需要安装C语言的编译环境
先检查有没有环境,注意系统需要连上外网。我有环境就不需要安装了

gcc --version

安装命令

yum install gcc

在这里插入图片描述
三、解压命令:tar -zxvf 包名

tar -zxvf redis-6.2.12.tar.gz

在这里插入图片描述
四、解压后进入目录:cd 你的redis目录

cd redis-6.2.12

五、用make命令把redis编译成C文件

make

在这里插入图片描述
如果有报错再把编译文件清除,注意没有报错不要清除!

make distclean

六、安装
他会默认安装到:/usr/local/bin 路径下

make install

在这里插入图片描述
查看安装完成
在这里插入图片描述

启动

前台启动不推荐了,就不写了,一般都是后台启动
一、先复制出一份配置文件
在这里插入图片描述
到etc下可以看到redis.conf文件
在这里插入图片描述
二、设置支持后台启动,daemonize no 改成 yes
vi命令修改

vi redis.conf

输出 /daem 可以快速搜索到,键盘上 insert 键按一次就可以修改了,将no改成yes
在这里插入图片描述
保存退出的方式,ctrl + c 退出编辑,然后输入:wq 就可以保存了
在这里插入图片描述
这边就完成修改了

三、启动服务
在这里插入图片描述
然后通过 ps 命令查看服务是否启动

ps -ef | grep redis

启动成功,忽略我几个 vi 没有关闭
在这里插入图片描述

redis-cli 客户端连接

出现 127.0.0.1:6379 就表示连上了
在这里插入图片描述


Redis介绍相关知识

端口6379

6379 是 "MERZ " 九宫格输入法对应的数字。Alessia Merz 是一位意大利舞女、女演员。 Redis 作者 Antirez
早年看电视节目,觉得 Merz 在节目中的一些话愚蠢可笑,Antirez 喜欢造“梗”用于平时和朋友们交流,于是造了一个词
“MERZ”,形容愚蠢,与 “stupid” 含义相同。后来 Antirez 重新定义了 “MERZ”
,形容”具有很高的技术价值,包含技艺、耐心和劳动,但仍然保持简单本质“。到了给 Redis 选择一个数字作为默认端口号时,Antirez
没有多想,把 “MERZ” 在手机键盘上对应的数字 6379 拿来用了。
默认16个数据库,类似数组下标从0开始,初始默认使用0号库

  • 使用命令 select 来切换数据库。如: select 15

  • 统一密码管理,所有库同样密码。

  • dbsize查看当前数据库的key的数量

  • flushdb清空当前库

  • flushall通杀全部库

Redis是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)
(与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用)
图 所有命令在一个队列里排队等待被执行
图2-1 所有命令在一个队列里排队等待被执行
不存在多个命令被同时执行的情况
图2-2 不存在多个命令被同时执行的情况

因为Redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。所以上面3个客户端命令的执行顺序是不确定的(如图2-1所示),但是可以确定不会有两条命令被同时执行(如图2-2所示),假设有两条 incr (自增)命令,无论怎么执行最终结果都是2,不会产生并发问题,这就是Redis单线程的基本模型。但是像发送命令、返回结果、命令排队肯定不像描述的这么简单,Redis使用了I/O多路复用技术来解决I/O的问题,下一节将进行介绍。

为什么单线程还能这么快

通常来讲,单线程处理能力要比多线程差,例如有10000斤货物,每辆车的运载能力是每次200斤,那么要50次才能完成,但是如果有50辆车,只要安排合理,只需要一次就可以完成任务。那么为什么 Redis 使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:

  • 第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
  • 第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如图2-3所示。
    图2-3 Redis使用IO多路复用和自身事件模型
    图2-3 Redis使用IO多路复用和自身事件模型
  • 第三,单线程避免了线程切换和竞态产生的消耗。

redis可执行文件

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

在这里插入图片描述
例如,如果要用6380作为端口启动Redis,那么可以执行:

# redis-server --port 6380

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

配置文件启动

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

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

这个启动方式在上一章节,redis安装、启动中有介绍了。

redis的基础配置

Redis有60多个配置,这里只给出一些重要的配置
在这里插入图片描述

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

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的强大功能将在之后进行详细介绍。

停止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持久化的特性,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

注意选择 nosave 或是 save。

Redis重大版本

Redis借鉴了Linux操作系统对于版本号的命名规则:版本号第二位如果是奇数,则为非稳定版本(例如2.7、2.9、3.1),如果是偶数,则为稳定版本(例如2.6、2.8、3.0、3.2)。当前奇数版本就是下一个稳定版本的开发版本,例如2.9版本是3.0版本的开发版本。所以我们在生产环境通常选取偶数版本的Redis,如果对于某些新的特性想提前了解和使用,可以选择最新的奇数版本。

1.Redis2.6

Redis2.6在2012年正式发布,经历了17个版本,到2.6.17版本,相比于Redis2.4,主要特性如下:

1)服务端支持Lua脚本。
2)去掉虚拟内存相关功能。
3)放开对客户端连接数的硬编码限制。
4)键的过期时间支持毫秒。
5)从节点提供只读功能。
6)两个新的位图命令:bitcount和bitop。
7)增强了redis-benchmark的功能:支持定制化的压测,CSV输出等功能。
8)基于浮点数自增命令:incrbyfloat和hincrbyfloat。
9)redis-cli可以使用–eval参数实现Lua脚本执行。
10)shutdown命令增强。
11)info可以按照section输出,并且添加了一些统计项。
12)重构了大量的核心代码,所有集群相关的代码都去掉了,cluster功能将会是3.0版本最大的亮点。
13)sort命令优化。

2.Redis2.8

Redis2.8在2013年11月22日正式发布,经历了24个版本,到2.8.24版本,相比于Redis2.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第二版,相比于Redis2.6的Redis Sentinel,此版本已经变成生产可用。

3.Redis3.0

Redis3.0在2015年4月1日正式发布,相比于Redis2.8主要特性如下:

注意: Redis3.0最大的改动就是添加Redis的分布式实现Redis Cluster,填补了Redis官方没有分布式实现的空白。Redis Cluster经历了4年才正式发布也是有原因的,具体可以参考Redis Cluster的开发日志
(http://antirez.com/news/79 )。

1)Redis Cluster:Redis的官方分布式实现。
2)全新的embedded string对象编码结果,优化小对象内存访问,在
特定的工作负载下速度大幅提升。
3)lru算法大幅提升。
4)migrate连接缓存,大幅提升键迁移的速度。
5)migrate命令两个新的参数copy和replace。
6)新的client pause命令,在指定时间内停止处理客户端请求。
7)bitcount命令性能提升。
8)config set设置maxmemory时候可以设置不同的单位(之前只能
是字节),例如config set maxmemory1gb。
9)Redis日志小做调整:日志中会反应当前实例的角色(master或
者slave)。
10)incr命令性能提升。

4.Redis3.2

Redis3.2在2016年5月6日正式发布,相比于Redis3.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.Redis4.0

可能出乎很多人的意料,Redis3.2之后的版本是4.0,而不是3.4、3.6、3.8。一般这种重大版本号的升级也意味着软件或者工具本身发生了重大变革,下面列出Redis4.0的新特性:

1)提供了模块系统,方便第三方开发者拓展Redis的功能,更多模块详见:http://redismodules.com 。
2)PSYNC2.0:优化了之前版本中,主从节点切换必然引起全量复制的问题。
3)提供了新的缓存剔除算法:LFU(Last Frequently Used),并对已有算法进行了优化。
4)提供了非阻塞del和flushall/flushdb功能,有效解决删除bigkey可能造成的Redis阻塞。
5)提供了RDB-AOF混合持久化格式,充分利用了AOF和RDB各自优势。
6)提供memory命令,实现对内存更为全面的监控统计。
7)提供了交互数据库功能,实现Redis内部数据库之间的数据置换。
8)Redis Cluster兼容NAT和Docker。


API的理解和使用

全局命令

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

1.查看所有键

keys *

2.键总数

dbsize

dbsize 命令在计算键总数时不会遍历所有键,而是直接获取 Redis 内置的键总数变量,所以 dbsize 命令的时间复杂度是 O(1)。而 keys 命令会遍历所有键,所以它的时间复杂度是 O(n),当 Redis 保存了大量键时,线上环境禁止使用。

3.检查键是否存在

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

exists {key}

4.删除键

返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回0,同时del命令可以支持删除多个键:

del {key} [key ...]

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:键不存在
127.0.0.1:6379> ttl hello
(integer) 7
# 还剩7秒
127.0.0.1:6379> ttl hello
(integer) -2
#已被删除

6.键的数据结构类型

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_exsit_key
none

数据结构和内部编码

type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构。
实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。
可以看到每种数据结构都有两种以上的内部编码实现,例如list数据结构包含了linkedlist和ziplist两种内部编码。同时有些内部编码,例如ziplist,可以作为多种外部数据结构的内部实现,可以通过object encoding命令查询内部编码:

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

在这里插入图片描述
图 Redis的5种数据结构
Redis数据结构和内部编码
图 Redis数据结构和内部编码

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

Redis 这样设计有两个好处:

  • 第一,可以改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如 Redis3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更为优秀的内部编码实现,而对外部用户来说基本感知不到。
  • 第二,多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为 linkedlist。

字符串

字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。如图所示,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。
图 字符串数据结构
图 字符串数据结构

常用命令

常用命令就不加小标题了,可以通过浏览器页面 ctrl + F 输入关键字,在笔记中搜索自己需要的命令

(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还提供了setex和setnx两个命令:

setex key seconds value
setnx key value

它们的作用和ex和nx选项是一样的。下面的例子说明了set、setnx、set 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 redis
(integer) 0

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

127.0.0.1:6379> set hello jedis xx
OK

setnx 和 setxx 在实际使用中有什么应用场景吗?以 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命令需要按照下图的方式来执行,具体耗时如下:
在这里插入图片描述
图 n次get命令执行模型

使用mget命令后,要执行n次get命令操作只需要按照下图的方式来完成,具体耗时如下:
在这里插入图片描述
图 一次mget命令执行模型

Redis 可以支撑每秒数万的读写操作,但是这指的是 Redis 服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,假设网络时间为1毫秒,命令时间为0.1毫秒(按照每秒处理1万条命令算),那么执行1000次 get 命令和1次mget命令的区别如下表,因为 Redis 的处理能力已经足够高,对于开发人员来说,网络可能会成为性能的瓶颈。

表 1000次get和1次get对比表
在这里插入图片描述

学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成 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服务端都要顺序执行。

不常用命令

(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}

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

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

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

127.0.0.1:6379> getrange redis 0 1
"be"

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

表 字符串类型命令时间复杂度
在这里插入图片描述

内部编码

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

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
有关字符串类型的内存优化技巧将在后面补充。

典型使用场景

场景1.缓存功能
下图是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
下图伪代码模拟了的访问过程:
Redis+MySQL组成的缓存存储架构
图 Redis+MySQL组成的缓存存储架构

该函数用于获取用户的基础信息:
1、首先从Redis获取用户信息
2、如果没有从Redis获取到用户信息,需要从MySQL中进行获取,并将结果回写到Redis,添加1小时(600)秒过期时间

UserInfo getUserInfo(longid){
	//定义键
	userRedisKey="user:info:"+id;
	//1、从Redis获取值
	value=redis.get(userRedisKey);
	if(value!=null){
		//将值进行反序列化为UserInfo并返回结果
		userInfo=deserialize(value);
		return userInfo;
	}
	//2、从MySQL获取用户信息
	userInfo=mysql.get(id);
	//将userInfo序列化,并存入Redis
	redis.setex(userRedisKey,3600,serialize(userInfo));
	//返回结果
	return userInfo;
}

场景2.计数
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。例如视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1:

long incrVideoCounter(longid){
	String key = "video:playCount:" + id;
	returnredis.incr(key);
}

开发提示

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

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

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

场景4.限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,如图所示。
图 短信验证码限速
图 短信验证码限速
此功能可以使用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次也可以采用类似的思路。
除了上面介绍的几种使用场景,字符串还有非常多的适用场景,开发人员可以结合字符串提供的相应命令充分发挥自己的想象力。

哈希

几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对结构,形如value={{field1,value1},…{fieldN,valueN}},Redis键值对和哈希类型二者的关系可以用图来表示。
字符串和哈希类型对比
图 字符串和哈希类型对比

注意

哈希类型中的映射关系叫作field-value,注意这里的value是指field对应的值,不是键对应的值,请注意value在不同上下文的作用。

哈希命令

(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命令,它们的关系就像set和setnx命令一样,只不过作用域由键变为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 ...]

hmset 和 hmget 分别是批量设置和获取 field-value,hmset 需要的参数是 key 和多对 field-value,hmget 需要的参数是 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将在后面介绍。

(10)hincrby hincrbyfloat

hincrby key field
hincrbyfloat key field

hincrby 和 hincrbyfloat,就像 incrby 和incrbyfloat 命令一样,但是它们的作用域是 filed。

(11)计算value的字符串长度(需要Redis3.2以上)

hstrlen key field

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

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

下表是哈希类型命令的时间复杂度,开发人员可以参考此表选择适合的命令。
在这里插入图片描述
在这里插入图片描述

哈希内部编码

哈希类型的内部编码有两种:

  • ·ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplistentries配置(默认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 64 byte...忽略..."
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"

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

哈希使用场景

下图为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。
图 关系型数据库表保存用户信息
图 关系型数据库表保存用户信息
如果将其用哈希类型存储,如下图所示。
图 使用哈希类型缓存用户信息
图 使用哈希类型缓存用户信息

相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的 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),如图所示。
  • ·关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。
    图 关系型数据库稀疏性
    图 关系型数据库稀疏性

开发人员需要将两者的特点搞清楚,才能在适合的场景使用适合的技术。到目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。

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

优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,hashtable 会消耗更多内存。

列表

列表(list)类型是用来存储多个有序的字符串,如图所示,a、b、c、d、e五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储232-1个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等(如图所示)。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
在这里插入图片描述
图 列表两端插入和弹出操作
在这里插入图片描述
图 子列表获取、删除等操作

列表类型有两个特点:
第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表,例如要获取图的第5个元素,可以执行lindex user:1:message4(索引从0算起)就可以得到元素e。
第二、列表中的元素可以是重复的,例如图(下面图错了)所示列表中包含了两个字符串a。
在这里插入图片描述
图 列表中可以包含重复元素(书上图错了,没有重复元素)

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

命令

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

表 列表的四种操作类型
在这里插入图片描述

1.添加操作

(1)从右边插入元素

rpush key value [value ...]

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

127.0.0. 1:6379> rpush listkey c b a
(integer) 3

lrange0-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>t 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

blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,它们除了弹出方向不同,使用方法基本相同,所以下面以 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 0
..阻塞
..

此时另一个客户端分别向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 0
1) "list:2"
2) "element2_1"

第二点,如果多个客户端对同一个键执行 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"

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

表 列表命令时间复杂度
在这里插入图片描述

列表内部编码

列表类型的内部编码有两种。

  • 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 e513
(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"

开发提示

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

有关列表类型的内存优化技巧将在后续详细介绍。

使用场景

1.消息队列
如图所示,Redis 的 lpush + brpop 命令组合即可实现阻塞队列,生产者客户端使用 lrpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
在这里插入图片描述
图 Redis 消息队列模型

2.文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

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

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

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

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

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

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

使用列表类型保存和获取文章列表会存在两个问题。
第一,如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用 Pipeline(后续会介绍)批量获取,或者考虑将文章数据序列化为
字符串类型,使用 mget 批量获取。
第二,分页获取文章列表时,lrange 命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用 Redis3.2 的 quicklist 内部编码实现,它结合 ziplist 和 linkedlist 的特点,获取列表中间范围的元素时也可以高效完成。

开发提示
实际上列表的使用场景很多,在选择时可以参考以下口诀:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpsh+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)

集合

集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。如下图所示,集合 user:1:follow 包含着 “it” 、 “music” 、 “his” 、 “sports” 四个元素,一个集合最多可以存储 232 -1 个元素。Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
在这里插入图片描述
图 集合类型

集合命令

下面将按照集合内和集合间两个维度对集合的常用命令进行介绍。

1.集合内操作

(1)添加元素

sadd key element [element ...]

返回结果为添加成功的元素个数,例如:

127.0.0.1:6379> exists myset
(integer) 0
127.0.0.1:6379> sadd myset a b c
(integer) 3
127.0.0.1:6379> sadd myset a b
(integer) 0

(2)删除元素

srem key element [element ...]

返回结果为成功删除元素个数,例如:

127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> srem myset hello
(integer) 0

(3)计算元素个数

scard key

scard 的时间复杂度为 O(1),它不会遍历集合所有元素,而是直接用 Redis 内部的变量,例如:

127.0.0.1:6379> scard myset
(integer) 1

(4)判断元素是否在集合中

sismember key element

如果给定元素 element 在集合内返回 1,反之返回 0,例如:

127.0.0.1:6379> sismember myset c
(integer) 1

(5)随机从集合返回指定个数元素

srandmember key [count]

[count] 是可选参数,如果不写默认为 1,例如:

127.0.0.1:6379> srandmember myset 2
1) "a"
2) "c"
127.0.0.1:6379> srandmember myset
"d"

(6)从集合随机弹出元素

spop key

spop 操作可以从集合中随机弹出一个元素,例如下面代码是一次 spop 后,集合元素变为 “d b a”:

127.0.0.1:6379> spop myset
"c"
127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "a"

需要注意的是 Redis 从 3.2 版本开始,spop 也支持 [count] 参数。
srandmember 和 spop 都是随机从集合选出元素,两者不同的是 spop 命令执行后,元素会从集合中删除,而 srandmember 不会。

(7)获取所有元素

smembers key

下面代码获取集合 myset 所有元素,并且返回结果是无序的:

127.0.0.1:6379> smembers myset
1) "d"
2) "b"
3) "a"

smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存在阻塞 Redis 的可能性,这时候可以使用 sscan 来完成,有关 sscan 命令后续介绍。

2.集合间操作

现在有两个集合,它们分别是 user:1:follow 和 user:2:follow:

127.0.0.1:6379> sadd user:1:follow it music his sports
(integer) 4
127.0.0.1:6379> sadd user:2:follow it news ent sports
(integer) 4

(1)求多个集合的交集

sinter key [key ...]

例如下面代码是求 user:1:follow 和 user:2:follow 两个集合的交集,返回结果是 sports、it:

127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "sports"
2) "it"

(2)求多个集合的并集

suinon key [key ...]

例如下面代码是求 user:1:follow 和 user:2:follow 两个集合的并集,返回结果是 sports、it、his、news、music、ent:

127.0.0.1:6379> sunion user:1:follow user:2:follow
1) "sports"
2) "it"
3) "his"
4) "news"
5) "music"
6) "ent"

(3)求多个集合的差集

sdiff key [key ...]

例如下面代码是求 user:1:follow 和 user:2:follow 两个集合的差集,返回结果是 music 和 his:

127.0.0.1:6379> sdiff user:1:follow user:2:follow
1) "music"
2) "his"

前面三个命令如图所示。
在这里插入图片描述
图 集合求交集、差集、并集

(4)将交集、并集、差集的结果保存

sinterstore destination key [key ...]  // 交集
suionstore destination key [key ...]  // 并集
sdiffstore destination key [key ...]  // 差集

集合间的运算在元素较多的情况下会比较耗时,所以 Redis 提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key 中,例如下面操作将 user:1:follow 和 user:2:follow 两个集合的交集结果保存在 user:1_2:inter 中,user:1_2:inter 本身也是集合类型:

127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow
(integer) 2
127.0.0.1:6379> type user:1_2:inter
set
127.0.0.1:6379> smembers user:1_2:inter
1) "it"
2) "sports"

至此有关集合的命令基本已经介绍完了,下表给出集合常用命令的时间复杂度,开发人员可以根据自身需求进行选择。

表 集合常用命令时间复杂度
在这里插入图片描述

集合内部编码

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于 set-max-intset-entries 配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。

下面用示例来说明:
1)当元素个数较少且都为整数时,内部编码为 intset:

127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding setkey
"intset"

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

127.0.0.1:6379> sadd setkey 1 2 3 4 5 6 ... 512 513
(integer) 509
127.0.0.1:6379> scard setkey
(integer) 513
127.0.0.1:6379> object encoding listkey
"hashtable"

2.2)当某个元素不为整数时,内部编码也会变为 hashtable:

127.0.0.1:6379> sadd setkey a
(integer) 1
127.0.0.1:6379> object encoding setkey
"hashtable"

有关集合类型的内存优化技巧将在后续详细介绍。

集合使用场景

集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。
下面使用集合类型实现标签功能的若干功能。
(1)给用户添加标签

sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
...

(2)给标签添加用户

sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...

开发提示

用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致,有关如何将两个命令放在一个事务,后续介绍事务以及Lua的使用方法。

(3)删除用户下的标签

srem user:1:tags tag1 tag5
...

(4)删除标签下的用户

srem tag1:users user:1
srem tag5:users user:1
...

(3)和(4)也是尽量放在一个事务执行。

(5)计算用户共同感兴趣的标签
可以使用 sinter 命令,来计算用户共同感兴趣的标签,如下代码所示:

sinter user:1:tags user:2:tags

开发提示

前面只是给出了使用 Redis 集合类型实现标签的基本思路,实际上一个标签系统远比这个要复杂得多,不过集合类型的应用场景通常为以下几种:

  • sadd=Tagging(标签)
  • spop/srandmember=Random item(生成随机数,比如抽奖)
  • sadd+sinter=Social Graph(社交需求)

有序集合

有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。如图所示,该有序集合包含 kris、mike、frank、tim、martin、tom,它们的分数分别是 1、91、200、220、250、251,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
在这里插入图片描述
图 有序集合

开发提示
有序集合中的元素不能重复,但是 score 可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。

表 给出了列表、集合和有序集合三者的异同点
在这里插入图片描述

有序集合命令

依旧按照集合内和集合外两个维度对有序集合的命令进行介绍。

1.集合内

(1)添加成员

zadd key score member [score member ...]

下面操作向有序集合 user:ranking 添加用户 tom 和他的分数 251:

127.0.0.1:6379> zadd user:ranking 251 tom
(integer) 1

返回结果代表成功添加成员的个数:

127.0.0.1:6379> zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin
(integer) 5

有关 zadd 命令有两点需要注意:

  • Redis3.2 为 zadd 命令添加了 nx、xx、ch、incr 四个选项:
    – nx:member 必须不存在,才可以设置成功,用于添加。
    – xx:member 必须存在,才可以设置成功,用于更新。
    – ch:返回此次操作后,有序集合元素和分数发生变化的个数。
    – incr:对 score 做增加,相当于后面介绍的 zincrby。
  • 有序集合相比集合提供了排序字段,但是也产生了代价,zadd 的时间复杂度为 O(log(n)),sadd 的时间复杂度为 O(1)。

(2)计算成员个数

zcard key

例如下面操作返回有序集合 user:ranking 的成员数为 5,和集合类型的 scard 命令一样,zcard 的时间复杂度为 O(1)。

127.0.0.1:6379> zcard user:ranking
(integer) 5

(3)计算某个成员的分数

zscore key member

tom 的分数为 251,如果成员不存在则返回 nil:

127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking test
(nil)

(4)计算成员的排名

zrank key member
zrevrank key member

zrank 是从分数从低到高返回排名,zrevrank 反之。例如下面操作中,tom 在 zrank 和 zrevrank 分别排名第 5 和第 0(排名从 0 开始计算)。

127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0

(5)删除成员

zrem key member [member ...]

下面操作将成员 mike 从有序集合 user:ranking 中删除。

127.0.0.1:6379> zrem user:ranking mike
(integer) 1

返回结果为成功删除的个数。

(6)增加成员的分数

zincrby key increment member

下面操作给 tom 增加了 9 分,分数变为了 260 分:

127.0.0.1:6379> zincrby user:ranking 9 tom
"260"

(7)返回指定排名范围的成员

zrange key start end [withscores]
zrevrange key start end [withscores]

有序集合是按照分值排名的,zrange 是从低到高返回,zrevrange 反之。下面代码返回排名最低的是三个成员,如果加上 withscores 选项,同时会返回成员的分数:

127.0.0.1:6379> zrange user:ranking 0 2 withscores
1) "kris"
2) "1"
3) "frank"
4) "200"
5) "tim"
6) "220"
127.0.0.1:6379> zrevrange user:ranking 0 2 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"

(8)返回指定分数范围的成员

zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]

其中 zrangebyscore 按照分数从低到高返回,zrevrangebyscore 反之。例如下面操作从低到高返回 200 到 221 分的成员,withscores 选项会同时返回每个成员的分数。[limit offset count] 选项可以限制输出的起始位置和个数:

127.0.0.1:6379> zrangebyscore user:ranking 200 tinf withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 221 200 withscores
1) "tim"
2) "220"
3) "frank"
4) "200"

同时 min 和 max 还支持开区间(小括号)和闭区间(中括号),-inf 和 +inf 分别代表无限小和无限大:

127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores
1) "tim"
2) "220"
3) "martin"
4) "250"
5) "tom"
6) "260"

(9)返回指定分数范围成员个数

zcount key min max

下面操作返回 200 到 221 分的成员的个数:

127.0.0.1:6379> zcount user:ranking 200 221
(integer) 2

(10)删除指定排名内的升序元素

zremrangebyrank key start end

下面操作删除第 start 到第 end 名的成员:

127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3

(11)删除指定分数范围的成员

zremrangebyscore key min max

下面操作将 250 分以上的成员全部删除,返回结果为成功删除的个数:

127.0.0.1:6379> zremrangebyscore user:ranking (250 +inf
(integer) 2
2.集合间的操作

将图的两个有序集合导入到 Redis 中。
在这里插入图片描述
图 有序集合 user:ranking:1 和 user:ranking:2

127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
(integer) 6
127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom
(integer) 4

(1)交集

zinterstore destination numkeys key [key ...] [weights weight [weight ...]] 
[aggregate sum|min|max]

这个命令参数较多,下面分别进行说明:

  • destination:交集计算结果保存到这个键。
  • numkeys:需要做交集计算键的个数。
  • key[key…]:需要做交集计算的键。
  • weights weight[weight…]:每个键的权重,在做交集计算时,每个键中的每个 member 会将自己分数乘以这个权重,每个键的权重默认是 1。
  • aggregate sum|min|max:计算成员交集后,分值可以按照 sum(和)、min(最小值)、max(最大值)做汇总,默认值是 sum。

下面操作对 user:ranking:1 和 user:ranking:2 做交集,weights 和 aggregate 使用了默认配置,可以看到目标键 user:ranking:1_inter_2 对分值做了 sum 操作:

127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1
user:ranking:2
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "168"
3) "martin"
4) "875"
5) "tom"
6) "1139"

如果想让 user:ranking:2 的权重变为 0.5,并且聚合效果使用 max,可以执行如下操作:

127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1
user:ranking:2 weights 1 0.5 aggregate max
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "91"
3) "martin"
4) "312.5"
5) "tom"
6) "444"

(2)并集

zunionstore destination numkeys key [key ...] [weights weight [weight ...]]
[aggregate sum|min|max]

该命令的所有参数和 zinterstore 是一致的,只不过是做并集计算,例如下面操作是计算 user:ranking:1 和 user:ranking:2 的并集,weights 和 aggregate 使用了默认配置,可以看到目标键 user:ranking:1_union_2 对分值做了 sum 操作:

127.0.0.1:6379> zunionstore user:ranking:1_union_2 2 user:ranking:1 user:ranking:2
(integer) 7
127.0.0.1:6379> zrange user:ranking:1_union_2 0 -1 withscores
1) "kris"
2) "1"
3) "james"
4) "8"
5) "mike"
6) "168"
7) "frank"
8) "200"
9) "tim"
10) "220"
11) "martin"
12) "875"
13) "tom"
14) "1139"

至此有序集合的命令基本介绍完了,下表是这些命令的时间复杂度,开发人员在使用对应的命令进行开发时,不仅要考虑功能性,还要了解相应的时间复杂度,防止由于使用不当造成应用方效率下降以及 Redis 阻塞。

表 有序集合命令的时间复杂度
在这里插入图片描述

有序集合内部编码

Bitmaps

数据结构模型

现代计算机用二进制(位)作为信息的基础单位,1个字节等于8位,例如“big”字符串是由3个字节组成,但实际在计算机存储时将其用二进制表示,“big”分别对应的ASCII码分别是98、105、103,对应的二进制分别是01100010、01101001和01100111,如图所示。
在这里插入图片描述
字符串“big”用二进制表示

许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis提供了Bitmaps这个“数据结构”可以实现对位的操作。把数据结构加上引号主要因为:

  • Bitmaps本身不是一种数据结构,实际上它就是字符串(如图所示),但是它可以对字符串的位进行操作。
  • Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。

字符串"big"用二进制表示
字符串"big"用二进制表示

命令

本节将每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id。

1.设置值
setbit key offset value

设置键的第offset个位的值(从0算起),假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图所示。
在这里插入图片描述
setbit使用

具体操作过程如下,unique:users:2016-04-05代表2016-04-05这天的独立访问用户的Bitmaps:

127.0.0.1:6379> setbit unique:users:2016-04-05 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 11 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 15 1
(integer) 0
127.0.0.1:6379> setbit unique:users:2016-04-05 19 1
(integer) 0

如果此时有一个userid=50的用户访问了网站,那么Bitmaps的结构变成了图3-12,第20位~49位都是0。
在这里插入图片描述
很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。

2.获取值
gitbit key offset

获取键的第offset位的值(从0开始算),下面操作获取id=8的用户是否在2016-04-05这天访问过,返回0说明没有访问过:

127.0.0.1:6379> getbit unique:users:2016-04-05 8
(integer) 0

由于offset=1000000根本就不存在,所以返回结果也是0:

127.0.0.1:6379> getbit unique:users:2016-04-05 1000000
(integer) 0
3.获取Bitmaps指定范围值为1的个数
bitcount [start][end]

下面操作计算2016-04-05这天的独立访问用户数量:

127.0.0.1:6379> bitcount unique:users:2016-04-05
(integer) 5

[start]和[end]代表起始和结束字节数,下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数,对应的用户id是11,15,19。

127.0.0.1:6379> bitcount unique:users:2016-04-05 1 3
(integer) 3
4.Bitmaps间的运算
bitop op destkey key[key....]

bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。假设2016-04-04访问网站的userid=1,2,5,9,如图所示。
2016-04-04访问网站的用户Bitmaps
2016-04-04访问网站的用户Bitmaps

下面操作计算出2016-04-04和2016-04-03两天都访问过网站的用户数量,如图3-14所示。

127.0.0.1:6379> bitop and unique:users:and:2016-04-04_03 unique: users:2016-04-03
unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:and:2016-04-04_03
(integer) 2

如果想算出2016-04-04和2016-04-03任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集,具体命令如下:

127.0.0.1:6379> bitop or unique:users:or:2016-04-04_03 unique:
users:2016-04-03 unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:or:2016-04-04_03
(integer) 6

在这里插入图片描述

5.计算Bitmaps中第一个值为targetBit的偏移量
bitpos key targetBit [start] [end]

下面操作计算2016-04-04当前访问网站的最小用户id:

127.0.0.1:6379> bitpos unique:users:2016-04-04 1
(integer) 1

除此之外,bitops有两个选项[start]和[end],分别代表起始字节和结束字节,例如计算第0个字节到第1个字节之间,第一个值为0的偏移量,从图3-13可以得知结果是id=0的用户。

127.0.0.1:6379> bitpos unique:users:2016-04-04 0 0 1
(integer) 0

在这里插入图片描述

HyperLogLog

HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法(Philippe
Flajolet (https://en.wikipedia.org/wiki/Philippe_Flajolet )在 The analysis of
a near-optimal cardinality estimation algorithm 这篇论文中提出),通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等。HyperLogLog提供了3个命令:pfadd、pfcount、pfmerge。例如2016-03-06的访问用户是uuid-1、uuid-2、uuid-3、uuid-4,2016-03-05的访问用户是uuid-4、uuid-5、uuid-6、uuid-7,如图3-15所示。
在这里插入图片描述

1.添加

pfadd key element [element …
]

pfadd用于向HyperLogLog添加元素,如果添加成功返回1:

127.0.0.1:6379> pfadd 2016_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1

2.计算独立用户数

pfcount key [key …
]

pfcount用于计算一个或多个HyperLogLog的独立总数,例如2016_03_06:unique:ids的独立总数为4:

127.0.0.1:6379> pfcount 2016_03_06:unique:ids
(integer) 4

如果此时向2016_03_06:unique:ids插入uuid-1、uuid-2、uuid-3、uuid-90,结果是5(新增uuid-90):

127.0.0.1:6379> pfadd 2016_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-90"
(integer) 1
127.0.0.1:6379> pfcount 2016_03_06:unique:ids
(integer) 5

当前这个例子内存节省的效果还不是很明显,下面使用脚本向HyperLogLog插入100万个id,插入前记录一下info memory:

127.0.0.1:6379> info memory # Memory
used_memory:835144
used_memory_human:815.57K
...向 2016_05_01:unique:ids插入100万个用户,每次插入1000条:
elements=""
key="2016_05_01:unique:ids"
for i in `seq 1 1000000`
do
	elements="${elements} uuid-"${i}
	if [[ $((i%1000)) == 0 ]];
	then
		redis-cli pfadd ${key} ${elements}
		elements=""
	fi
done

当上述代码执行完成后,可以看到内存只增加了15K左右:

127.0.0.1:6379> info memory
# Memory
used_memory:850616
used_memory_human:830.68K

但是,同时可以看到pfcount的执行结果并不是100万:

127.0.0.1:6379> pfcount 2016_05_01:unique:ids
(integer) 1009838

可以对100万个uuid使用集合类型进行测试,代码如下:

elements=""
key="2016_05_01:unique:ids:set"
for i in `seq 1 1000000`
do
	elements="${elements} "${i}
	if [[ $((i%1000)) == 0 ]];
	then
		redis-cli sadd ${key} ${elements}
		elements=""
	fi
done

可以看到内存使用了84MB:

127.0.0.1:6379> info memory
# Memory
used_memory:88702680
used_memory_human:84.59M

但独立用户数为100万:

127.0.0.1:6379> scard 2016_05_01:unique:ids:set
(integer) 1000000

表3-6列出了使用集合类型和HperLogLog统计百万级用户的占用空
间对比。

表3-6 集合类型和HyperLogLog占用空间对比
在这里插入图片描述
可以看到,HyperLogLog内存占用量小得惊人,但是用如此小空间来估算如此巨大的数据,必然不是100%的正确,其中一定存在误差率。Redis官方给出的数字是0.81%的失误率。

3.合并
pfmerge destkey sourcekey [sourcekey ...]

pfmerge 可以求出多个 HyperLogLog 的并集并赋值给 destkey,例如要计算2016年3月5日和3月6日的访问独立用户数,可以按照如下方式来执行,可以看到最终独立用户数是7:

127.0.0.1:6379> pfadd 2016_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"
(integer) 1
127.0.0.1:6379> pfadd 2016_03_05:unique:ids "uuid-4" "uuid-5" "uuid-6" "uuid-7"
(integer) 1
127.0.0.1:6379> pfmerge 2016_03_05_06:unique:ids 2016_03_05:unique:ids
2016_03_06:unique:ids
OK
127.0.0.1:6379> pfcount 2016_03_05_06:unique:ids
(integer) 7

HyperLogLog内存占用量非常小,但是存在错误率,开发者在进行数据结构选型时只需要确认如下两条即可:

  • 只为了计算独立总数,不需要获取单条数据。
  • 可以容忍一定误差率,毕竟HyperLogLog在内存的占用量上有很大的优势。

发布订阅

Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息,如图3-16所示。Redis提供了若干命令支持该功能,在实际应用开发时,能够为此类问题提供实现方法。
在这里插入图片描述

命令

Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。

1.发布消息

publish channel message

下面操作会向channel:sports频道发布一条消息“Tim won thechampionship”,返回结果为订阅者个数,因为此时没有订阅,所以返回结果为0:

127.0.0.1:6379> publish channel:sports "Tim won the championship"
(integer) 0

2.订阅消息

subscribe channel [channel ...]

订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了channel:sports频道:

127.0.0.1:6379> subscribe channel:sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel:sports"
3) (integer) 1

此时另一个客户端发布一条消息:

127.0.0.1:6379> publish channel:sports "James lost the championship"
(integer) 1

当前订阅者客户端会收到如下消息:

127.0.0.1:6379> subscribe channel:sports
Reading messages... (press Ctrl-C to quit)
...
1) "message"
2) "channel:sports"
3) "James lost the championship"

如果有多个客户端同时订阅了channel:sports,整个过程如图3-17所示。
在这里插入图片描述
有关订阅命令有两点需要注意:

  • 客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。
  • 新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化。
开发提示

和很多专业的消息队列系统(例如Kafka、RocketMQ)相比,Redis的发布订阅略显粗糙,例如无法实现消息堆积和回溯。但胜在足够简单,如果当前场景可以容忍的这些缺点,也不失为一个不错的选择。

取消订阅

unsubscribe [channel [channel ...]]

客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功
后,不会再收到该频道的发布消息:

127.0.0.1:6379> unsubscribe channel:sports
1) "unsubscribe"
2) "channel:sports"
3) (integer) 0

4.按照模式订阅和取消订阅

psubscribe pattern [pattern...]
punsubscribe [pattern [pattern ...]]

除了 subcribe 和 unsubscribe 命令,Redis 命令还支持 glob 风格的订阅命令 psubscribe 和取消订阅命令 punsubscribe,例如下面操作订阅以 it 开头的所有频道:

127.0.0.1:6379> psubscribe it*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "it*"
3) (integer) 1

5.查询订阅

(1)查看活跃的频道
pubsub channels [pattern]

所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式:

127.0.0.1:6379> pubsub channels
1) "channel:sports"
2) "channel:it"
3) "channel:travel"
127.0.0.1:6379> pubsub channels channel:*r*
1) "channel:sports"
2) "channel:travel"
(2)查看频道订阅数
pubsub numsub [channel ...]

当前channel:sports频道的订阅数为2:

127.0.0.1:6379> pubsub numsub channel:sports
1) "channel:sports"
2) (integer) 2
(3)查看模式订阅数
pubsub numpat

当前只有一个客户端通过模式来订阅:

127.0.0.1:6379> pubsub numpat
(integer) 1

GEO(地理信息定位)

Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。

1.增加地理位置信息

geoadd key longitude latitude member [longitude latitude member ...]

longitude、latitude、member分别是该地理位置的经度、纬度、成员,表3-7展示5个城市的经纬度。
在这里插入图片描述
cities:locations是上面5个城市地理位置信息的集合,现向其添加北京的地理位置信息:

127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing
(integer) 1

返回结果代表添加成功的个数,如果 cities:locations 没有包含 beijing,那么返回结果为1,如果已经存在则返回 0:

127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing
(integer) 0

如果需要更新地理位置信息,仍然可以使用geoadd命令,虽然返回结果为0。
geoadd命令可以同时添加多个地理位置信息:

127.0.0.1:6379> geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02
shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding
(integer) 4

2.获取地理位置信息

geopos key member [member ...]

下面操作会获取天津的经维度:

127.0.0.1:6379> geopos cities:locations tianjin
1) 1) "117.12000042200088501"
2) "39.0800000535766543"

3.获取两个地理位置的距离

geodist key member1 member2 [unit]

其中unit代表返回结果的单位,包含以下四种:

  • m(meters)代表米。
  • km(kilometers)代表公里。
  • mi(miles)代表英里。
  • ft(feet)代表尺。

下面操作用于计算天津到北京的距离,并以公里为单位:

127.0.0.1:6379> geodist cities:locations tianjin beijing km
"89.2061"

4.获取指定位置范围内的地理信息位置集合

georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist]
[withhash] [COUNT count] [asc|desc] [store key] [storedist key]

georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist]
[withhash] [COUNT count] [asc|desc] [store key] [storedist key]

georadius 和 georadiusbymember 两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是 georadius 命令的中心位置给出了具体的经纬度,georadiusbymember 只需给出成员即可。其中 radiusm|km|ft|mi 是必需参数,指定了半径(带单位),这两个命令有很多可选参数,如下所示:

  • withcoord:返回结果中包含经纬度。
  • withdist:返回结果中包含离中心节点位置的距离。
  • withhash:返回结果中包含geohash,有关geohash后面介绍。
  • COUNT count:指定返回结果的数量。
  • asc|desc:返回结果按照离中心节点的距离做升序或者降序。
  • store key:将返回结果的地理位置信息保存到指定键。
  • storedist key:将返回结果离中心节点的距离保存到指定键。

下面操作计算五座城市中,距离北京150公里以内的城市:

127.0.0.1:6379> georadiusbymember cities:locations beijing 150 km
1) "beijing"
2) "tianjin"
3) "tangshan"
4) "baoding"

5.获取geohash

geohash key member [member ...]

Redis使用geohash将二维经纬度转换为一维字符串,下面操作会返回beijing的geohash值。

127.0.0.1:6379> geohash cities:locations beijing
1) "wx4ww02w070"

geohash有如下特点:

  • GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
127.0.0.1:6379> type cities:locations
zset
  • 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右。
    在这里插入图片描述
  • 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
  • geohash编码和经纬度是可以相互转换的。

Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。

6.删除地理位置信息

zrem key member

GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。

sprint boot 整合 redis

引入依赖

<!-- redis -->
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

<!-- 连接池 -->
<dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
      <version>2.6.0</version>
</dependency>

配置文件

#Redis服务器地址
spring.redis.host=192.168.47.33
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认位0)一共16个数据库
spring.redis.database=0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(复数表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

redis 配置类

尚硅谷配置

public class RedisConfig

私下配置

@Configuration
@EnableCaching
public class RedisTemplateConfiguration {
    private Map<String, RedisCacheConfiguration> ttlMap;
    @Autowired(required = false)
    private CacheExpireFunction[] cacheTtl;
    private List<String> flushCache = new ArrayList<>();

    @Bean
    @Primary
    public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        //使用fastjson序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        FastJson2JsonRedisSerializer fastJson2JsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(fastJson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(fastJson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        return redisTemplate;
    }

    @PostConstruct
    public void init() {

        if (cacheTtl != null) {
            ttlMap = new HashMap<>();
            ttlMap = Stream.of(cacheTtl)
                    .map(CacheExpireFunction::addExpires)
                    .peek(cacheTime -> {
                        if (cacheTime.getNeedFlush()) {
                            flushCache.add(cacheTime.getName());
                        }
                    })
                    .collect(Collectors.toMap(CacheTime::getName
                            , cacheTime -> createRedisCacheConfiguration(cacheTime.getName(),cacheTime.getExpire().getSeconds())));
        }
    }

    @Bean
    public KeyGenerator KeyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory){
        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(factory);
        //默认CACHE信息缓存配置
        RedisCacheConfiguration defaultCacheConfig = createRedisCacheConfiguration(RedisConstant.CACHE,RedisConstant.DEFAULT_EXPIRATION);
        //初始化RedisCacheManager
        if(ttlMap != null) {
           return RedisCacheManager.builder(redisCacheWriter)
                    .cacheDefaults(defaultCacheConfig).initialCacheNames(ttlMap.keySet()).withInitialCacheConfigurations(ttlMap)
                    .build();
        }
        return RedisCacheManager.builder(redisCacheWriter).cacheDefaults(defaultCacheConfig).build();
    }

    /**
     * 创建RedisCacheConfiguration
     * @param delimiter 缓存redis前缀
     * @param entryTtl  缓存过期时间
     * @return
     */
    private RedisCacheConfiguration createRedisCacheConfiguration(String delimiter,long entryTtl){

        RedisPrefix cachePrefix = new RedisPrefix(delimiter);
        //序列化方式2
        FastJson2JsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer);
        // redis 链式写法
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(entryTtl))
                .computePrefixWith(cachePrefix)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
                .serializeValuesWith(pair);
        return  defaultCacheConfig;
    }
}

public class RedisConstant {
    public final static String CACHE = "cache" + RedisConstant.DELIMITER;
    public final static String DELIMITER = ":";
    public final static Long DEFAULT_EXPIRATION = 3600L;
}
import java.time.Duration;

public class CacheTime {
    private String name;
    private Boolean needFlush;
    private Duration expire;

    public CacheTime(String name, Duration expire, Boolean needFlush) {
        this.name = name;
        this.needFlush = needFlush;
        this.expire = expire;
    }

    public CacheTime(String name, Duration expire) {
        this.name = name;
        this.expire = expire;
        this.needFlush = false;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Boolean getNeedFlush() {
        return needFlush;
    }

    public void setNeedFlush(Boolean needFlush) {
        this.needFlush = needFlush;
    }

    public Duration getExpire() {
        return expire;
    }

    public void setExpire(Duration expire) {
        this.expire = expire;
    }
}
@FunctionalInterface
public interface CacheExpireFunction {
    CacheTime addExpires();
}

redis 事务

事务

熟悉关系型数据库的读者应该对事务比较了解,简单地说,事务表示一组动作,要么全部执行,要么全部不执行。例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。

Redis 提供了简单的事务功能,将一组需要一起执行的命令放到 multi 和 exec 两个命令之间。multi 命令代表事务开始,exec 命令代表事务结束,它们之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED

可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中。如果此时另一个客户端执行 sismember user:a:follow user:b返回结果应该为0。

127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0

只有当exec执行后,用户A关注用户B的行为才算完成,如下所示返回的两个结果对应sadd命令。

127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1

如果要停止事务的执行,可以使用discard命令代替exec命令即可。

127.0.0.1:6379> discard
OK
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0

如果事务中的命令出现错误,Redis的处理机制也不尽相同。

1.命令错误

例如下面操作错将set写成了sett,属于语法错误,会造成整个事务无法执行,key和counter的值未发生变化:

127.0.0.1:6388> mget key counter
1) "hello"
2) "100"
127.0.0.1:6388> multi
OK
127.0.0.1:6388> sett key world
(error) ERR unknown command 'sett'
127.0.0.1:6388> incr counter
QUEUED
127.0.0.1:6388> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6388> mget key counter
1) "hello"
2) "100"

2.运行时错误

例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> zadd user:b:fans 1 user:a
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1

可以看到Redis并不支持回滚功能,sadd user:a:follow user:b命令已经执行成功,开发人员需要自己修复这类问题。

watch 命令(乐观锁)

有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题,表3-2展示了两个客户端执行命令的时序。
在这里插入图片描述
可以看到“客户端-1”在执行multi之前执行了watch命令,“客户端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果为nil),整个代码如下所示:

#T1:客户端
1
127.0.0.1:6379> set key "java"
OK
#T2:客户端
1
127.0.0.1:6379> watch key
OK
#T3:客户端
1
127.0.0.1:6379> multi
OK
#T4:客户端
2
127.0.0.1:6379> append key python
(integer) 11
#T5:客户端
1
127.0.0.1:6379> append key jedis
QUEUED
#T6:客户端
1
127.0.0.1:6379> exec
(nil)
#T7:客户端
1
127.0.0.1:6379> get key
"javapython"

Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了Redis的“keep it simple”的特性,下一小节介绍的Lua脚本同样可以实现事务的相关功能,但是功能要强大很多。

Lua 用法简述

Lua 语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大,所以许多应用都选用它作为脚本语言,尤其是在游戏领域,例如大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎,Web服务器Nginx将Lua语言作为扩展,增强自身功能。Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令,在这之前,必须修改源码。在介绍如何在Redis中使用Lua脚本之前,有必要对Lua语言的使用做一个基本的介绍。

我已经写了一篇文章做了简单介绍,这里不写太杂乱,可以点击链接 跳转Lua简单介绍

Redis与Lua

1.在Redis中使用Lua

在Redis中执行Lua脚本有两种方法:eval和evalsha。

(1)eval
eval 脚本内容 key 个数 key 列表 参数列表

下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性:

127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world "hello redisworld"

此时KEYS[1]=“redis”,ARGV[1]=“world”,所以最终的返回结果是"hello redisworld"。

如果Lua脚本较长,还可以使用redis-cli–eval直接执行文件。

eval命令和–eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,整个过程如图所示。

eval命令执行Lua脚本过程
eval命令执行Lua脚本过程

(2)evalsha

除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如图所示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每
次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
使用evalsha执行Lua脚本过程
使用evalsha执行Lua脚本过程

加载脚本 :script load命令可以将脚本内容加载到Redis内存中,例如下面将lua_get.lua加载到Redis中,得到SHA1为:“7413dc2440db1fea7c0a0bde841fa68eefaf149c”

# redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"

执行脚本 :evalsha的使用方法如下,参数使用SHA1值,执行逻辑和eval一致。

evalsha 脚本 SHA1值 key个数 key列表 参数列表

所以只需要执行如下操作,就可以调用lua_get.lua脚本:

127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"

2.Lua的Redis API

Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用redis.call调用了Redis的set和get操作:

redis.call("set", "hello", "world")
redis.call("get", "hello")

放在Redis的执行效果如下:

127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"

除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本,所以在
实际开发中要根据具体的应用场景进行函数的选择。

开发提示
Lua 可以使用 redis.log 函数将 Lua 脚本的日志输出到 Redis 的日志文件中,但是一定要控制日志级别。
Redis3.2 提供了 Lua Script Debugger 功能用来调试复杂的Lua脚本,具体可以参考: http://redis.io/topics/ldb

案例

Lua脚本功能为Redis开发和运维人员带来如下三个好处:

  • Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命
    令。
  • Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以
    将这些命令常驻在Redis内存中,实现复用的效果。
  • Lua脚本可以将多条命令一次性打包,有效地减少网络开销。

下面以一个例子说明Lua脚本的使用,当前列表记录着热门用户的id,假设这个列表有5个元素,如下所示:

127.0.0.1:6379> lrange hot:user:list 0 -1
1) "user:1:ratio"
2) "user:8:ratio"
3) "user:3:ratio"
4) "user:99:ratio"
5) "user:72:ratio"

user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:

127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio
user:72:ratio
1) "986"
2) "762"
3) "556"
4) "400"
5) "101"

现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行,此功能可以利用Lua脚本来实现。

1)将列表中所有元素取出,赋值给mylist:

local mylist = redis.call("lrange", KEYS[1], 0, -1)

2)定义局部变量count=0,这个count就是最后incr的总次数:

local count = 0

3)遍历mylist中所有元素,每次做完count自增,最后返回count:

for index,key in ipairs(mylist)
do
	redis.call("incr",key)
	count = count + 1
end
return count

将上述脚本写入lrange_and_mincr.lua文件中,并执行如下操作,返回结果为5。

redis-cli --eval lrange_and_mincr.lua hot:user:list
(integer) 5

执行后所有用户的热度自增1:

127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio
user:72:ratio
1) "987"
2) "763"
3) "557"
4) "401"
5) "102"

本节给出的只是一个简单的例子,在实际开发中,开发人员可以发挥自己的想象力创造出更多新的命令。

redis 事务三特性

  1. 单独的隔离操作
  • 事务中的所有命令都会序列化、按顺序执行。在事务执行过程中,不会被其他客户端发送来的命令打断。
  1. 没有隔离级别的概念
  • 队列中的命令没有提交之前都不会被执行,只有提交后才开始执行队列中的命令。
    因为mysql数据库中会互相读互相操作,redis中是单独的隔离操作,事务是比较简单的,没有任何交互,所以没有隔离的概念。
  1. 不保证原子性
  • 事务中如果有一条命令执行失败,后面的其他命令照常会被执行,没有回滚。
  • 比如执行五条命令,中间有一条失败了,不影响整个队列的命令,其他正确的命令一定会被执行。

简单说:redis的多个客户端操作就是你操作你的,我操作我的,不会互相干扰。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值