简介
背景
我们现在的项目架构中,基本上是Web服务器(Tomcat)和数据库独立部署,独占服务器资源,随着用户数的增长,并发读写数据库,会加大数据库访问压力,导致性能的下降,严重时直接导致系统宕机,例如:
此时,我们可以在Tomcat同服务器上中增加本地缓存,并在外部增加分布式缓存,缓存热门数据。也就是通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。例如:
基于这样的一种架构设计,于是类似redis的一些分布式数据库就诞生了。
Redis 概述
Redis(Remote Dictionary Server )即远程字典服务,是 C 语言开发的一个开源的key/value存储系统(官网:http://redis.io),是一个分布式缓存数据库。Redis
是一个开源的(遵守 BSD
协议)、支持网络、可基于内存亦可持久化的日志型、Key-Value
数据库。由于它是基于内存的所以它要比基于磁盘读写的数据库效率更快。在DB-Engines.com的数据库排行中, Redis上升排行第七,如图所示:
Redis 提供了一些丰富的数据结构,包括 lists、sets、ordered sets 以及 hashes ,当然还有和 Memcached 一样的 strings 结构。Redis 当然还包括了对这些数据结构的丰富操作。
Redis 常被称作是一款数据结构服务器(data structure server)。Redis 的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和 有序集合(sorted sets)等数据类型。
对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。
Redis框架如图:
Redis 的优点
- 性能极高:Redis 能支持超过 100K+ 每秒的读写频率。
- 丰富的数据类型:Redis 支持二进制案例的 Strings,Lists,Hashes,Sets 及 Ordered Sets 数据类型操作。
- 原子:Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作全并后的原子性执行。
- 丰富的特性:Redis 还支持 publish/subscribe,通知,key 过期等等特性。
Redis为什么这么快
Redis 到底有多快
大家可能都知道 Redis
很快,可是 Redis
到底能有多快呢,比如 Redis
的吞吐量能达到多少?我想这就不是每个人都能说的上来一个具体的数字了。
Redis
官方提供了一个测试脚本,可以供我们测试 Redis
的吞吐量:
redis-benchmark -q -n 100000
:测试常用命令的吞吐量。redis-benchmark -t set,lpush -n 100000 -q
:测试Redis
处理set
和lpush
命令的吞吐量。redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
:测试Redis
处理Lua
脚本等吞吐量。
下图就是我这边执行第一条命令的自测结果,可以看到大部分命令的吞吐量都可以达到 4
万以上,也就是说每秒钟可以处理 4
万次以上请求:
但是如果你以为这就是 Redis
的真实吞吐量,那就错了。实际上,Redis
官方的测试结果是可以达到 10
万的吞吐量,下图就是官方提供的一个基准测试结果(纵坐标就是吞吐量,横坐标是连接数):
Redis 是单线程还是多线程
这个问题比较经典,因为在很多人的认知里,Redis
就是单线程的。然而 Redis
从 4.0
版本开始就有了多线程的概念,虽然处理命令请求的核心模块确实是保证了单线程执行,然而在其它许多地方已经有了多线程,比如:在后台删除对象,通过 Redis
模块实现阻塞命令,生成 dump
文件,以及 6.0
版本中网络 I/O
实现了多线程等,而且在未来 Redis
应该会有越来越多的模块实现多线程。
所谓的单线程,只是说 Redis
处理客户端的请求(即执行命令)时,是单线程去执行的,并不是说整个 Redis
都是单线程。
Redis 为什么选择使用单线程来执行请求
Redis
为什么会选择使用单线程呢?这是因为 CPU
成为 Redis
瓶颈的情况并不常见,成为 Redis
瓶颈的通常是内存或网络带宽。例如,在一个普通的 Linux
系统上使用 pipelining
命令,Redis
可以每秒完成 100
万个请求,所以如果我们的应用程序主要使用 O(N)
或 O(log(N))
复杂度的命令,它几乎不会使用太多的 CPU
。
那么既然 CPU
不会成为瓶颈,理所当然的就没必要去使用多线程来执行命令,这里需要明确的一个问题就是:多线程一定会比单线程快吗?答案是不一定。因为多线程也是有代价的,最直接的两个代价就是线程的创建和销毁(当然可以通过线程池来一定程度减少频繁的创建线程和销毁线程)以及线程的上下文切换。
在我们的日常系统中,主要可以区分为两种:CPU
密集型 和 IO
密集型:
CPU
密集型:这种系统就说明CPU
的利用率很高,那么使用多线程反而会增加上下文切换而带来额外的开销,所以使用多线程效率可能会不升反降。
举个例子:假如你现在在干活,你一直不停的在做一件事,需要 1
分钟可以做完,但是你中途总是被人打断,需要花 1
秒钟时间步行到旁边去做另一件事,假如这件事也需要 1
分钟,那么你因为反复切换做两件事,每切换一次就要花 1
秒钟,最后做完这 2
件事的时间肯定大于 2
分钟(取决于中途切换的次数),但是如果中途不被打断,你做完一件事再去做另一件事,那么你最多只需要切换 1
次,也就是 2
分 1
秒就能做完。
IO
密集型:IO
操作也可以分为磁盘IO
和网络IO
等操作。大部分IO
操作的特点是比较耗时且CPU
利用率不高,所以Redis 6.0
版本网络IO
会改进为多线程。至于磁盘IO
,因为Redis
中的数据都存储在内存(也可以持久化),所以并不会过多的涉及到磁盘操作。
举个例子:假如你现在给树苗浇水,你每浇完一次水之后就需要等别人给你加水之后你才能继续浇,那么假如这个等待过程需要 5
秒钟,也就是说你浇完一次水就可以休息 5
秒钟,而你切换去做另一件事来回只需要 2
秒,那么你完全可以先去做另一件事,做完之后再回来继续浇水,这样就可以充分利用你空闲的 5
秒钟时间,从而提升了效率。
使用多线程还会带来一个问题就是数据的安全性,所以多线程编程都会涉及到锁竞争,由此也会带来额外的开销。
什么是 IO 多路复用机制
I/O
指的是网络 I/O
, 多路指的是多个 TCP
连接(如 Socket
),复用指的是复用一个或多个线程。I/O
多路复用的核心原理就是不再由应用程序自己来监听连接,而是由服务器内核替应用程序监听。
在 Redis
中,其多路复用有多种实现,如:select
,epoll
,evport
,kqueue
等。
我们用去餐厅吃饭的例子来解释一下 I/O
多路复用机制(点餐人相当于客户端,餐厅的厨房相当于服务器,厨师就是线程)。
- 阻塞
IO
:张三去餐厅吃饭,点了一道菜,这时候他啥事也不干了,就是一直等,等到厨师炒好菜,他就把菜端走开始吃饭了。也就是在菜被炒好之前,张三被阻塞了,这就是BIO
(阻塞IO
),效率非常低下。 - 非阻塞
IO
:张三去餐厅吃饭,点了一道菜,这时候张三他不会一直等,找了个位置坐下,刷刷抖音,打打电话,做点其它事,然后每隔一段时间就去厨房问一下自己的菜好了没有。这种就属于非阻塞IO
,这种方式虽然可以提高性能,但是如果有大量IO
都来定期轮询,也会给服务器造成非常大的负担。 - 事件驱动机制:张三去餐厅吃饭,点了一道菜,这时候他找了个位置坐下来等,接下来厨房(服务器)有两种做法:
- 厨房把菜做好了直接把菜端出去,但是端菜的人并不知道这道菜是谁的,于是就挨个询问顾客,这就是多路复用中的
select
模型,不过select
模型最多只能监听1024
个socket
(poll
模型解决了这个限制问题)。 - 厨房把菜做好了直接把菜放在窗口上,大喊一声:“某某菜做好了,是谁的快过来拿。”这时候听到通知的顾客就会自己去拿,这就是多路复用中的
epoll
模型。
- 厨房把菜做好了直接把菜端出去,但是端菜的人并不知道这道菜是谁的,于是就挨个询问顾客,这就是多路复用中的
需要注意的是:在 IO
多路复用机制下,客户端可以阻塞也可以选择不阻塞(大部分场景下是阻塞 IO
),这个要具体情况具体分析,但是在多路复用机制下,服务端就可以通过多线程(上面示例中可以多几个厨师同时炒菜)来提升并发效率。
Redis 中 I/O 多路复用的应用
Redis
服务器是一个事件驱动程序,服务器需要处理两类事件:文件事件和时间事件。
- 文件事件:
Redis
服务器和客户端(或其它服务器)进行通信会产生相应的文件事件,然后服务器通过监听并处理这些事件来完成一系列的通信操作。 - 时间事件:
Redis
内部的一些在给定时间之内需要进行的操作。
Redis
的文件事件处理器以单线程的方式运行,其内部使用了 I/O
多路复用程序来同时监听多个套接字(Socket
)连接,提升了性能的同时又保持了内部单线程设计的简单性。下图就是文件事件处理器的示意图:
I/O
多路复用程序虽然会同时监听多个 Socket
连接,但是其会将监听的 Socket
都放到一个队列里面,然后通过这个队列有序的、同步的将每个 Socket
对应的事件传送给文件事件分派器,再由文件事件分派器分派给对应的事件处理器进行处理,只有当一个 Socket
所对应的事件被处理完毕之后,I/O
多路复用程序才会继续向文件事件分派器传送下一个 Socket
所对应的事件,这也可以验证上面的结论,处理客户端的命令请求是单线程的方式逐个处理,但是事件处理器内并不是只有一个线程。
Redis 为什么这么快
Redis
为什么这么快的原因前面已经基本提到了,现在我们再总结一下:
Redis
是纯内存结构的,避免了磁盘I/O
等耗时操作。Redis
命令处理的核心模块为单线程,减少了锁竞争,以及频繁创建线程和销毁线程的代价,减少了线程上下文切换的消耗。- 采用了
I/O
多路复用机制,大大提升了并发效率。
版本及参考说明
Redis的次版本号(第一个小数点后的数字)为偶数的版本是稳定版本(2.4、2.6等),奇数为非稳定版本(2.5、2.7),一般推荐在生产环境使用稳定版本。最新版本6.2.2,新增了stream的处理方式,性能更高。Redis官方是不支持windows平台的,windows版本是由微软自己建立的分支,基于官方的Redis源码上进行编译、发布、维护的,所以windows平台的Redis版本要略低于官方版本。
Redis 相关参考网址如下所示:
Bootnb 相关:https://www.runoob.com/redis/redis-tutorial.html
Redis 官网:https://redis.io/
源码地址:https://github.com/redis/redis
Redis 在线测试:http://try.redis.io/
Redis 命令参考:http://doc.redisfans.com/
Redis 安装(Linux)
Redis 的安装步骤
redis 手动安装的话非常简单,以 redis-4.0.9 版本为例。
首先进入 root 目录并下载 Redis 的程序包:
sudo su
cd
wget https://labfile.oss.aliyuncs.com/courses/106/redis-4.0.9.tar.gz
在目录下,解压安装包,生成新的目录 redis-4.0.9:
tar -xzvf redis-4.0.9.tar.gz
进入解压之后的目录,进行编译:
cd redis-4.0.9
make
说明:如果没有明显的错误,则表示编译成功。
操作截图:
查看重要文件
在 Redis 安装完成后,注意一些重要的文件,可用 ls 命令查看。
- 服务端:src/redis-server
- 客户端:src/redis-cli
- 默认配置文件:redis.conf
设置环境变量
为了今后能更方便地打开 Redis 服务器和客户端,可以将 src 目录下的 redis-server 和 redis-cli 添加进环境变量所属目录里面。
cp redis-server /usr/local/bin/
cp redis-cli /usr/local/bin/
添加完成后在任何目录下输入 redis-server
可启动服务器,输入 redis-cli
可启动客户端。
运行测试
在前面的步骤设置完成后可以运行测试(非必须),确认 Redis 的功能是否正常。
cd /root/redis-4.0.9
make test
如果提示 You need tcl 8.5 or newer in order to run the Redis test
可以安装 tcl,然后再进行测试
sudo apt-get install tcl -y
这是在我本地虚拟机运行 make test
的结果:
Redis安装(Windows)
一、下载windows版本的Redis
官网上不提供windows版本的,现在官网没有下载地址,只能在github上下载,官网只提供linux版本的下载 官网下载地址:
这里我选择的是x64-3.2.100,下载的时候下载msi(不要下载zip的压缩包)
建议:在github上公开项目下载速度非常慢,我100M光钎每秒速度不到10K
可以在CSDN上下载 地址:
选择第一个Redis-x64-3.2.100.msi
二、安装Redis
1.首先双击现在完的安装程序
2.点击next
3.点击接受 继续next
4.设置Redis的服务端口 默认为6379 默认就好,单击next
5.选择安装的路径,并且打上勾(这个非常重要),添加到path是把Redis设置成windows下的服务,不然你每次都要在该目录下启动命令redis-server redis.windows.conf,但是只要一关闭cmd窗口,redis就会消失,这样就比较麻烦。
6.设置Max Memory,然后next进入安装
如果redis的应用场景是作为db使用,那不要设置这个选项,因为db是不能容忍丢失数据的。
如果作为cache缓存那就得看自己的需要(我这里设置了1024M的最大内存限制)
指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区。
7.安装完成
三、测试所安装的Redis
如果你是和我一样通过msi文件的安装,你可以在计算机管理→服务与应用程序→服务 看到Redis正在运行
你也可以将它停止,(不停止会出现错误代码为18012的错误,表示本机端口6379被占用)
然后在cmd窗口进入Redis的安装路径的根目录
输入命令redis-server.exe redis.windows.conf,出现下图证明Redis服务启动成功
下面进行测试:
你可以在Redis的安装根目录下找到redis-cli.exe文件启动(我用的是这种方法)
或在cmd中先进入Redis的安装根目录用命令**redis-cli.exe -h 192.168.10.61 -p 6379(注意换成自己的IP)**的方式打开
测试方法:设置键值对 取出键值对 (我这里键值对是peng)
四、测试成功,安装完成
Mac安装Redis
Redis初始操作
启动redis服务
Docker 环境下的启动(docker环境启动多个需要运行多个容器):
docker start redis01 #底层也是通过redis-server启动,start单词后的redis01为容器名
docker 中查看redis 服务
docker ps
查看启动的redis进程信息
ps -ef|grep redis
root 3511 1 0 16:29 ? 00:00:01 redis-server *:6379
root 3515 1 0 16:29 ? 00:00:01 redis-server 127.0.0.1:6380
进入redis容器
docker exec -it redis01 bash #redis01 为容器名
登陆redis服务
登陆本地redis
redis-cli
或者
redis-cli -p 6379
或者
redis-cli -p 6379 -a password #-a后面为password,此操作需要开启redis.conf文件中的 requirepass选项
登陆远程redis
redis-cli -h ip -p 6379 -a password
查看redis信息
首先登陆redis,然后输入info指令,例如
127.0.0.1:6379> info #查看当前redis节点的详细配置信息
清空redis屏幕
清除redis屏幕内容
127.0.0.1:6379> clear
退出redis服务
退出redis服务,例如
127.0.0.1:6379> exit
关闭redis服务
关闭redis服务,例如:
127.0.0.1:6379> shutdown
系统帮助
可以基于help指令查看相关指令帮助,例如
127.0.0.1:6379> help
redis-cli 2.8.19
Type: "help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics
"quit" to exit
123456
127.0.0.1:6379> help type
TYPE key
summary: Determine the type stored at key
since: 1.0.0
group: generic
Redis数据存储操作
在 Redis 中,命令大小写不敏感。
简易数据存取
基于查看redis中的key
127.0.0.1:6379> keys *
(empty list or set)
基于key/value形式存储数据
127.0.0.1:6379> set test1 123
OK
127.0.0.1:6379> set test2 ab
OK
127.0.0.1:6379> keys *
1) "test1"
2) "test2"
基于key获取redis中存储的数据
127.0.0.1:6379> get test1
"123"
127.0.0.1:6379> get test2
"ab"
127.0.0.1:6379> get test3
(nil)
127.0.0.1:6379>
清除redis中的数据
删除指定key对应的数据
127.0.0.1:6379> del test1 #test1为key的名字
OK
清除当前数据库数据
127.0.0.1:6379> flushdb
OK
清除所有数据库数据
127.0.0.1:6379> flushall
OK
适合全体类型的常用命令
启动 redis 服务和 redis-cli 命令界面继续后续实验:
sudo service redis-server start
sudo su
cd
redis-cli
EXISTS and DEL
exists
key:判断一个 key 是否存在,存在返回 1,否则返回 0。
del
key:删除某个 key,或是一系列 key,比如:del key1 key2 key3 key4。成功返回 1,失败返回 0(key 值不存在)。
> set mykey hello
> exists mykey
> del mykey
> exists mykey
操作截图:
TYPE and KEYS
type
key:返回某个 key 元素的数据类型(none:不存在,string:字符,list:列表,set:元组,zset:有序集合,hash:哈希),key 不存在返回空。
keys
key—pattern:返回匹配的 key 列表,比如:keys foo* 表示查找 foo 开头的 keys。
> set mykey x
> type mykey
> keys my*
> del mykey
> keys my*
> type mykey
操作截图:
RANDOMKEY and CLEAR
randomkey
:随机获得一个已经存在的 key,如果当前数据库为空,则返回空字符串。
> randomkey
操作截图:
clear
:清除界面。
> clear
RENAME and RENAMENX
rename oldname newname
:更改 key 的名字,新键如果存在将被覆盖。 renamenx oldname newname
:更改 key 的名字,新键如果存在则更新失败。
比如这里 randomkey 结果为 mylist,将此 key 值更名为 newlist。
> randomkey
> rename mylist newlist
> exists mylist
> exists newlist
操作截图:
DBSIZE
dbsize
:返回当前数据库的 key 的总数。
> dbsize
操作截图:
Redis 时间相关命令/Key有效时间设计
下面我们将会学习 Redis 时间相关命令。
实际工作中我们经常要控制redis中key的有效时长,例如秒杀操作的计时,缓存数据的有效时长等。
限定 key 生存时间
这同样是一个无视数据类型的命令,对于临时存储很有用处。避免进行大量的 DEL 操作。
Expire (设置生效时长-单位秒)
语法:EXPIRE key seconds
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 10
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 5
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 2
127.0.0.1:6379> ttl bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>
其中,TTL查看key的剩余时间,当返回值为-2时,表示键被删除。
当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。
expire
:设置某个 key 的过期时间(秒),比如:expire bruce 1000 表示设置 bruce 这个 key 1000 秒后系统自动删除,注意:如果在还没有过期的时候,对值进行了改变,那么那个值会被清除。
> set key some-value
> expire key 10
> get key # 马上执行此命令
> get key # 10s后执行此命令
操作截图:
结果显示:执行 expire 命令后,马上 get 会显示 key 存在;10 秒后再 get 时,key 已经被自动删除。
查询 key 剩余生存时间
限时操作可以在 set 命令中实现,并且可用 ttl 命令查询 key 剩余生存时间。
ttl
:查找某个 key 还有多长时间过期,返回时间单位为秒。
> set key 100 ex 30
> ttl key
> ttl key
操作截图:
清除 key
flushdb
:清空当前数据库中的所有键。 flushall
:清空所有数据库中的所有键。
> flushdb
> flushall
取消时长设置
Persist (取消时长设置)
通过persist让对特定key设置的生效时长失效。
语法:PERSIST key
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 60
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 49
127.0.0.1:6379> persist bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -1
127.0.0.1:6379>
其中,设置新的数据时需要重新设置该key的生存时间,重新设置值也会清除生存时间。
单位毫秒
pexpire (单位毫秒)
pexpire 让key的生效时长以毫秒作为计量单位,这样可以做到更精确的时间控制。例如,可应用于秒杀场景。
语法:PEXPIRE key milliseconds
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> pexpire bomb 10000
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 6
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>
Redis 设置相关命令
Redis 有其配置文件,可以通过 client-command 窗口查看或者更改相关配置。下面介绍相关命令。
CONFIG GET and CONFIG SET
config get
:用来读取运行 Redis 服务器的配置参数。 config set
:用于更改运行 Redis 服务器的配置参数。 auth
:认证密码。
下面针对 Redis 密码的示例:
> config get requirepass # 查看密码
> config set requirepass test123 # 设置密码为 test123
> config get requirepass # 报错,没有认证
> auth test123 # 认证密码
> config get requirepass
操作截图:
由结果可知,刚开始时 Reids 并未设置密码,密码查询结果为空。然后设置密码为 test123,再次查询报错。经过 auth 命令认证后,可正常查询。
可以通过修改 Redis 的配置文件 redis.conf 修改密码。
config get
命令是以 list 的 key-value 对显示的,如查询数据类型的最大条目:
> config get *max-*-entries*
操作截图:
重置报告
config resetstat
:重置数据统计报告,通常返回值为“OK”。
> CONFIG RESETSTAT
操作截图:
查询信息
info [section]
:查询 Redis 相关信息。
info 命令可以查询 Redis 几乎所有的信息,其命令选项有如下:
- server: Redis server 的常规信息
- clients: Client 的连接选项
- memory: 存储占用相关信息
- persistence: RDB and AOF 相关信息
- stats: 常规统计
- replication: Master/Slave 请求信息
- cpu: CPU 占用信息统计
- cluster: Redis 集群信息
- keyspace: 数据库信息统计
- all: 返回所有信息
- default: 返回常规设置信息
若命令参数为空,info 命令返回所有信息。
> info keyspace
> info server
操作截图:
Redis安全性设置
涉及到客户端连接是需要指定密码的(由于 redis 速度相当的快,一秒钟可以 150K 次的密码尝试,所以需要设置一个强度很大的密码)。
设置密码的方式有两种:
- 使用
config set
命令的 requirepass 参数,具体格式为config set requirepass [password]"
。 - 在 redis.conf 文件中设置 requirepass 属性,后面为密码。
输入认证的方式也有两种:
- 登录时可以使用
redis-cli -a password
。 - 登录后可以使用
auth password
。
设置密码
第一种密码设置方式在上一个实验中已经提到(在 CONFIG SET 命令讲解的实例),此处我们来看看第二种方式设置密码。
首先需要进入 Redis 的安装目录,然后修改配置文件 redis.conf
。根据 grep 命令的结果,使用 vim 编辑器修改 “# requirepass foobared” 为 “requirepass test123”,然后保存退出。
sudo grep -n requirepass /etc/redis/redis.conf
sudo vim /etc/redis/redis.conf
编辑 redis.conf
的结果:
重启 redis-server 与 redis-cli
重启 redis server:
sudo service redis-server restart
进入到 redis-cli 交互界面进行验证:
$ redis-cli
> info
> auth test123
> info
> exit
操作截图:
结果表明第一次 info 命令失败,在 auth 认证之后 info 命令正常返回,最后退出 redis-cli。
另外一种密码认证方式:
$ redis-cli -a test123
> info
操作截图:
常用数据类型
Redis作为一种key/value结构的数据存储系统,为了便于对数据进行进行管理,提供了多种数据类型。然后,基于指定类型存储我们项目中产生的数据,例如用户的登陆信息,购物车信息,商品详情信息等等。
Reids中基础数据结构包含字符串、散列,列表,集合,有序集合。工作中具体使用哪种类型要结合具体场景。
现在我们一一讲解:Redis keys 是采用二进制安全,这就意味着你可以使用任何二进制序列作为重点,比如:“foo” 可以联系一个 JPEG 文件;空字符串也是一个有效的密钥。
一个简单的字符串,为什么 Redis 要设计的如此特别
Redis 的 9 种数据类型
Redis
中支持的数据类型到 6.0.6
版本,一共有 9
种。分别是:
- Binary-safe strings(二进制安全字符串)
- Lists(列表)
- Sets(集合)
- Sorted sets(有序集合)
- Hashes(哈希)
- Bit arrays (or simply bitmaps)(位图)
- HyperLogLogs
- geospatial
- Streams
虽然这里列出了 9
种,但是基础类型就是前面 5
种。后面的 4
种是基于前面 5
种基本类型及特定的算法来实现的特殊类型。
而在 5
种基础类型之中,又尤其以字符串类型最为常用,且 key
值只能为字符串对象,所以要想深入的了解 Redis
的特性,字符串对象是首先需要学习的。
String类型操作实践
字符串类型是redis中最简单的数据类型,它存储的值可以是字符串,其最大字符串长度支持到512M。基于此类型,可以实现博客的字数统计,将日志不断追加到指定key,实现一个分布式自增iid,实现一个博客的的点赞操作等
二进制安全字符串
Redis
是基于 C
语言进行开发的,而 C
语言中的字符串是二进制不安全的,所以 Redis
就没有直接使用 C
语言的字符串,而是自己编写了一个新的数据结构来表示字符串,这种数据结构称之为简单动态字符串(Simple dynamic string),简称 sds
。
什么是二进制安全的字符串
在 C
语言中,字符串采用的是一个 char
数组(柔性数组)来存储字符串,而且字符串必须要以一个空字符串 \0
来结尾。字符串并不记录长度,所以如果想要获取一个字符串的长度就必须遍历整个字符串,直到遇到第一个 \0
为止(\0
不会计入字符串长度),故而获取字符串长度的时间复杂度为 O(n)
。
正因为 C
语言中是以遇到的第一个空字符 \0
来识别是否到了字符串末尾,因此其只能保存文本数据,不能保存图片、音频、视频和压缩文件等二进制数据,否则可能出现字符串不完整的问题,所以其是二进制不安全的。
Redis
中为了实现二进制安全的字符串,对原有 C
语言中的字符串实现做了改进。如下所示就是一个旧版本的 sds
字符串的结构定义:
struct sdshdr{
int len;//记录buf数组已使用的长度,即SDS的长度(不包含末尾的'\0')
int free;//记录buf数组中未使用的长度
char buf[];//字节数组,用来保存字符串
}
经过改进之后,如果想要获取 sds
的长度不用去遍历 buf
数组了,直接读取 len
属性就可以得到长度,时间复杂度一下就变成了 O(1)
,而且因为判断字符串长度不再依赖空字符 \0
,所以其能存储图片、音频、视频和压缩文件等二进制数据,不用担心读取到的字符串不完整。
需要注意的是,sds
依然遵循了 C
语言字符串以 \0
结尾的惯例,这么做是为了方便复用 C
语言字符串原生的一些 API,换言之就是在 C
语言中会以碰到的第一个 \0
字符作为当前字符串对象的结尾,所以如果一些二进制数据就可能会出现读取字符串不完整的现象,而 sds
会以长度来判断是否到字符串末尾。
在 Redis 3.2
之后的版本,Redis
对 sds
又做了优化,按照存储空间的大小拆分成为了 sdshdr5
、sdshdr8
、sdshdr16
、sdshdr32
、sdshdr64
,分别用来存储大小为:32
字节(2
的 5
次方),256
字节(2
的 8
次方),64KB
(2
的 16
次方),4GB
大小(2
的 32
次方)以及 2
的 64
次方大小的字符串(因为目前版本 key
和 value
都限制了最大 512MB
,所以 sdshdr64
暂时并未使用到)。 sdshdr5
只被应用在了 Redis
的 key
中,value
中不会被使用到,因为 sdshdr5
和其它类型也不一样,其并没有存储未使用空间,所以比较适用于使用大小固定的场景(比如 key
值):
任意选择其中一种数据类型,其字段代表含义如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; //已使用空间大小
uint8_t alloc; //总共申请的空间大小(包括未使用的)
unsigned char flags; //用来表示当前sds类型是sdshdr8还是sdshdr16等
char buf[]; //真实存储字符串的字节数组
};
可以看到相比较于 Redis 3.2
版本之前的 sds
,主要是修改了 free
属性然后新增了一个 flags
标记来区分当前的 sds
类型。
sds 空间分配策略
C
语言中因为字符串内部没有记录长度,所以如果扩充字符串非常容易造成缓冲区溢出(buffer overflow)。
请看下面这张图,假设下图就是内存里面的连续空间,可以很明显的看到,此时 wolf
和 Redis
两个字符串之间只有三个空位,那么这时候如果我们要将 wolf
字符串修改为 lonelyWolf
,那么就需要 6
个空间,这时候下面这个空间是放不下的,必须要重新申请空间。但是假如说程序员忘了申请空间,或者说申请到的空间依然不够,那么就会出现后面的 Redis
字符串中的 Red
被覆盖了:
同样的,假如要缩小字符串的长度,那么也需要重新申请释放内存。否则,字符串一直占据着未使用的空间,会造成内存泄露。
C
语言避免缓存区溢出和内存泄露完全依赖于人为,很难把控,但是使用 sds
就不会出现这两个问题,因为当我们操作 sds
时,其内部会自动执行空间分配策略,从而避免了上述两种情况的出现。
空间预分配
空间预分配指的是当我们通过 api
对 sds
进行扩展空间时,假如未使用空间不够用,那么程序不仅会为 sds
分配必须要的空间,还会额外分配未使用空间,未使用空间分配大小主要有两种情况:
- 假如扩大长度之后的
len
属性小于等于1MB
(即 1024 * 1024),那么就会同时分配和len
属性一样大小的未使用空间(此时buf
数组已使用空间 = 未使用空间)。 - 假如扩大长度之后的
len
属性大于1MB
,那么就会分配1MB
未使用空间大小。
执行空间预分配策略的好处是**提前分配了未使用空间备用后,就不需要每次增大字符串都需要分配空间,减少了内存重分配的次数。
惰性空间释放
惰性空间释放指的是当我们需要通过 api
减小 sds
长度的时候,程序并不会立即释放未使用的空间,而只是更新 free
属性的值,这样空间就可以留给下一次使用。而为了防止出现内存溢出的情况,sds
单独提供给了 api
让我们在有需要的时候去真正的释放内存。
sds 和 C 语言字符串区别
下面表格中列举了 Redis
中的 sds
和 C
语言中实现的字符串的区别:
C 字符串 | SDS |
---|---|
只能保存文本类不含空字符串 \0 数据 |
可以保存文本或者二进制数据,允许包含空字符串 \0 |
获取字符串长度的复杂度为 O(n) |
获取字符串长度的复杂度为 O(1) |
操作字符串可能会造成缓冲区溢出 | 不会出现缓冲区溢出情况 |
修改字符串长度 N 次,必然需要 N 次内存重分配 |
修改字符串长度 N 次,最多需要 N 次内存重分配 |
可以使用 C 字符串相关的所有函数 |
可以使用 C 字符串相关的部分函数 |
sds 是如何被存储的
在 Redis
中所有的数据类型都是将对应的数据结构进行了再一次包装,创建了一个字典对象来存储,sds
也不例外。每次创建一个 key-value
键值对,Redis
都会创建两个对象,一个是键对象,一个是值对象。而且需要注意的是在 Redis
中,值对象并不是直接存储,而是被包装成 redisObject
对象,并同时将键对象和值对象通过 dictEntry
对象进行封装,如下就是一个 dictEntry
对象:
typedef struct dictEntry {
void *key;//指向key,即sds
union {
void *val;//指向value
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;//指向下一个key-value键值对(哈希值相同的键值对会形成一个链表,从而解决哈希冲突问题)
} dictEntry;
redisObject
对象的定义为:
typedef struct redisObject {
unsigned type:4;//对象类型(4位=0.5字节)
unsigned encoding:4;//编码(4位=0.5字节)
unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节)
void *ptr;//指向底层实际的数据存储结构,如:sds等(8字节)
} robj;
当我们在 Redis
客户端中执行命令 set name lonely_wolf
,就会得到下图所示的一个结构(省略了部分属性):
看到这个图想必大家会有疑问,这里面的 type
和 encoding
到底是什么呢?其实这两个属性非常关键,Redis
就是通过这两个属性来识别当前的 value
到底属于哪一种基本数据类型,以及当前数据类型的底层采用了何种数据结构进行存储。
type 属性
type
属性表示对象类型,其对应了 Redis
当中的 5
种基本数据类型:
类型属性 | 描述 | type 命令返回值 |
---|---|---|
REDIS_STRING | 字符串对象 | string |
REDIS_LIST | 列表对象 | list |
REDIS_HASH | 哈希对象 | hash |
REDIS_SET | 集合对象 | set |
REDIS_ZSET | 有序集合对象 | zset |
可以看到,这就是对应了我们 5
种常用的基本数据类型。
encoding 属性
Redis
当中每种数据类型都是经过特别设计的,相信大家看完这个系列也会体会到 Redis
设计的精妙之处。字符串在我们眼里是非常简单的一种数据结构了,但是 Redis
却把它优化到了极致,为了节省空间,其通过编码的方式定义了三种不同的存储方式:
编码属性 | 描述 | object encoding命令返回值 |
---|---|---|
OBJ_ENCODING_INT | 使用整数的字符串对象 | int |
OBJ_ENCODING_EMBSTR | 使用 embstr 编码实现的字符串对象 |
embstr |
OBJ_ENCODING_RAW | 使用 raw 编码实现的字符串对象 |
raw |
int
编码:当我们用字符串对象存储的是整型,且能用8
个字节的long
类型进行表示(即2
的63
次方减1
),则Redis
会选择使用int
编码来存储,此时redisObject
对象中的ptr
指针直接替换为long
类型。我们想想8
个字节如果用字符串来存储只能存8
位,也就是千万级别的数字,远远达不到2
的63
次方减1
这个级别,所以如果都是数字,用long
类型会更节省空间。embstr
编码:当字符串对象中存储的是字符串,且长度小于44
(Redis 3.2
版本之前是39
)时,Redis
会选择使用embstr
编码来存储。raw
编码:当字符串对象中存储的是字符串,且长度大于44
时,Redis
会选择使用raw
编码来存储。
讲了半天理论,接下来让我们一起来验证下这些结论。首先启动 Redis
:
sudo redis-server /etc/redis/redis.conf
redis-cli
连接上 Redis
之后依次输入以下命令:
set name lonely_wolf
type name
object encoding name
得到如下所示结果:
可以发现当前的数据类型就是 string
,普通字符串因为长度小于 44
,所以采用的是 embstr
编码。
再依次输入如下命令:
set num 1111111111
type num
object encoding num
set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa //长度 44(注释不要输入)
object encoding address
set address aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa //长度 45(注释不要输入)
object encoding address
最后得到如下所示效果:
可以发现,当输入纯数字的时候,采用的是 int
编码,而字符串小于等于 44
则为 embstr
,大于 44
则为 raw
编码。
字符串对象中除了上面提到的纯整数和字符串,还可以存储浮点型类型,所以字符串对象可以存储以下三种类型:
- 字符串
- 整数
- 浮点数
而当我们的 value
为整数时,还可以使用原子自增命令来实现 value
的自增,这个命令在实际开发过程中非常实用。
incr key
:将key
的值自增1
。incrby key n
:将key
的值自增n
。
不过这两个命令只能用在 value
为整数的场景,当 value
不是整数时则会报错。
embstr 编码为什么从 39 位修改为 44 位
embstr
编码中,redisObject
和 sds
是连续的一块内存空间,这块内存空间 Redis
限制为了 64
个字节,而 redisObject
固定占了 16 字节(上面定义中有标注),Redis 3.2
版本之前的 sds
占了 8
个字节,再加上字符串末尾 \0
占用了 1
个字节,所以:64-16-8-1=39
字节。
Redis 3.2
版本之后 sds
做了优化,对于 embstr
编码会采用 sdshdr8
来存储,而 sdshdr8
占用的空间只有 24
位:3
字节(len + alloc + flag)+ \0
字符(1 字节),所以最后就剩下了:64-16-3-1=44
字节。
embstr 编码和 raw 编码的区别
embstr
编码是一种优化的存储方式,其在申请空间的时候因为 redisObject
和 sds
两个对象是一个连续空间,所以只需要申请 1
次空间(同样的,释放内存也只需要 1
次),而 raw
编码因为 redisObject
和 sds
两个对象的空间是不连续的,所以使用的时候需要申请 2
次空间(同样的,释放内存也需要 2
次)。但是使用 embstr
编码时,假如需要修改字符串,那么因为 redisObject
和 sds
是在一起的,所以两个对象都需要重新申请空间,为了避免这种情况发生,embstr
编码的字符串是只读的,不允许修改。
依次输入如下命令来验证一下编码转换:
set addr china
object encoding addr
append addr -beijing //在addr所对应的值后面追加“-beijing”(注释不要输入)
get addr
object encoding addr
得到如下所示结果:
上图中的示例我们看到,对一个 embstr
编码的字符串对象进行 append
操作时,长度还没有达到 45
,但是编码已经被修改为 raw
了,这就是因为 embstr
编码是只读的,如果需要对其修改,Redis
内部会将其修改为 raw
编码之后再操作。同样的,如果是操作 int
编码的字符串之后,导致 long
类型无法存储时(int
类型不再是整数或者长度超过 2
的 63
次方减 1
时),也会将 int
编码修改为 raw
编码。
PS:需要注意的是,编码一旦升级(int–>embstr–>raw),即使后期再把字符串修改为符合原编码能存储的格式时,编码也不会回退。
Redis strings
字符串是一种最基本、最常用的 Redis 值类型。
Redis
对字符串对象进行了特别设计,这就是 sds
,一种二进制安全的字符串对象。
Redis 字符串是二进制安全的,这意味着一个 Redis 字符串能包含任意类型的数据,例如: 一张经过 base64 编码的图片或者一个序列化的 Ruby 对象。通过这样的方式,Redis 的字符串可以支持任意形式的数据,但是对于过大的文件不适合存入 redis,一方面系统内存有限,另外一方面字符串类型的值最多能存储 512M 字节的内容。
启动 redis-cli 来看看 Redis strings 数据类型。
$ sudo service redis-server start
$ sudo su
$ cd
$ redis-cli
> set mykey somevalue
> get mykey
操作截图:
如上例所示,可以使用 set 和 get 命令来创建和检索 strings。注意:set 命令将取代现有的任何已经存在的 key。set 命令还有一个提供附加参数的选项,我们能够让 set 命令只有在没有相同 key 的情况下成功,反之亦然,可以让 set 命令在有相同 key 值的情况下成功:
> set mykey newval nx
> set mykey newval xx
操作截图;
即使 string 是 Redis 的基本类型,也可以对其进行一些有趣的操作,例如加法器:
> set counter 100 # 初始化
> incr counter # +1
> incr counter # +1
> incrby counter 50 # +50 自定义计数
操作截图:
incr 命令让 the value 成为一个整数,运行一次 incr 便加一。incrby 命令便是一个加法运算。类似的命令如减法运算为: decr 和 decrby。
Redis 可以运用 mset 和 mget 命令一次性完成多个 key-value 的对应关系,使用 mget 命令,Redis 返回一个 value 数组:
> mset a 10 b 20 c 30
> mget a b c
操作截图:
incr/incrby
当存储的字符串是整数时,redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增后的值。
语法:INCR key
127.0.0.1:6379> set num 1
(integer) 1
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> keys *
1) "num"
127.0.0.1:6379> incr num
127.0.0.1:6379>
说明,如果num不存在,则自动会创建,如果存在自动+1。
指定增长系数
语法:INCRBY key increment
127.0.0.1:6379> incrby num 2
(integer) 5
127.0.0.1:6379> incrby num 2
(integer) 7
127.0.0.1:6379> incrby num 2
(integer) 9
127.0.0.1:6379>
1234567
decr/decrby
减少指定的整数
DECR key 按照默认步长(默认为1)进行递减
DECRBY key decrement 按照指定步长进行递减
127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379> decr num
(integer) 9
127.0.0.1:6379> decrby num 3
append
向尾部追加值。如果键不存在则创建该键,其值为写的value,即相当于SET key value。返回值是追加后字符串的总长度。
语法:APPEND key value
127.0.0.1:6379> keys *
1) "num"
2) "test1"
3) "test"
127.0.0.1:6379> get test
"123"
127.0.0.1:6379> append test "abc"
(integer) 6
127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379>
strlen
字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0。
语法:STRLEN key
127.0.0.1:6379> get test
"123abc"
127.0.0.1:6379> strlen test
(integer) 6
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> set tnt ""
OK
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> exists tnt
(integer) 1
127.0.0.1:6379>
mset/mget
同时设置/获取多个键值
语法:MSET key value [key value …]
MGET key [key …]
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) "1"
2) "2"
3) "3"
127.0.0.1:6379>
小节面试分析
- 博客的字数统计如何实现?(strlen)
- 如何将审计日志不断追加到指定key?(append)
- 你如何实现一个分布式自增id?(incr-雪花算法)
- 如何实现一个博客的的点赞操作?(incr,decr)
Hash类型应用实践
Redis散列类型相当于Java中的HashMap,实现原理跟HashMap一致,一般用于存储对象信息,存储了字段(field)和字段值的映射,一个散列类型可以包含最多232-1个字段。
Redis Hashes
Redis Hashes 是字符串字段和字符串值之间的映射,因此它们是展现对象的完整数据类型。例如一个有名、姓、年龄等等属性的用户:一个带有一些字段的 hash 仅仅需要一块很小的空间存储,因此你可以存储数以百万计的对象在一个小的 Redis 实例中。哈希主要用来表现对象,它们有能力存储很多对象,因此你可以将哈希用于许多其它的任务。
> hmset user:1000 username antirez birthyear 1977 verified 1
> hget user:1000 username
> hget user:1000 birthyear
> hgetall user:1000
操作截图:
hmset 命令设置一个多域的 hash 表,hget 命令获取指定的单域,hgetall 命令获取指定 key 的所有信息。hmget 类似于 hget,只是返回一个 value 数组。
> hmget user:1000 username birthyear no-such-field
操作截图:
同样可以根据需要对 hash 表的表项进行单独的操作,例如 hincrby: (原本 birthyear 为 1977,见上一图)
> hincrby user:1000 birthyear 10
> hincrby user:1000 birthyear 10
操作截图:
hset/hget/hgetall
语法结构
HSET key field value
HGET key field
HMSET key field value [field value…]
HMGET key field [field]
HGETALL key
HSET和HGET赋值和取值
127.0.0.1:6379> hset user username chenchen
(integer) 1
127.0.0.1:6379> hget user username
"chenchen"
127.0.0.1:6379> hset user username chen
(integer) 0
127.0.0.1:6379> keys user
1) "user"
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379>
127.0.0.1:6379> hset user age 18
(integer) 1
127.0.0.1:6379> hset user address "xi'an"
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
3) "age"
4) "18"
3) "address"
4) "xi'an"
127.0.0.1:6379>
HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0。
hincrby
127.0.0.1:6379> hdecrby article total 1 #执行会出错
127.0.0.1:6379> hincrby article total -1 #没有hdecrby自减命令
(integer) 1
127.0.0.1:6379> hget article total #获取值
hmset/hmget
HMSET和HMGET设置和获取对象属性
127.0.0.1:6379> hmset person username tony age 18
OK
127.0.0.1:6379> hmget person age username
1) "18"
2) "tony"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>
注意:上面HMGET字段顺序可以自行定义
hexists
属性是否存在
127.0.0.1:6379> hexists killer
(error) ERR wrong number of arguments for 'hexists' command
127.0.0.1:6379> hexists killer a
(integer) 0
127.0.0.1:6379> hexists user username
(integer) 1
127.0.0.1:6379> hexists person age
(integer) 1
127.0.0.1:6379>
hdel
删除属性
127.0.0.1:6379> hdel user age
(integer) 1
127.0.0.1:6379> hgetall user
1) "username"
2) "chen"
127.0.0.1:6379> hgetall person
1) "username"
2) "tony"
3) "age"
4) "18"
127.0.0.1:6379>
hkeys/hvals
只获取字段名HKEYS或字段值HVALS
127.0.0.1:6379> hkeys person
1) "username"
2) "age"
127.0.0.1:6379> hvals person
1) "tony"
2) "18"
2.3.8 hlen
元素个数
127.0.0.1:6379> hlen user
(integer) 1
127.0.0.1:6379> hlen person
(integer) 2
127.0.0.1:6379>
小节面试分析
- 发布一篇博客需要写内存吗?(需要,hmset)
- 浏览博客内容会怎么做?(hmget)
- 如何判定一篇博客是否存在?(hexists)
- 删除一篇博客如何实现?(hdel)
- 分布式系统中你登录成功以后是如何存储用户信息的?(hmset)
List类型应用实践
Redis的list类型相当于java中的LinkedList,其原理就就是一个双向链表。支持正向、反向查找和遍历等操作,插入删除速度比较快。经常用于实现热销榜,最新评论等的设计。
Redis Lists
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),lpush 命令插入一个新的元素到头部,而 rpush 命令插入一个新元素到尾部。当这两个操作中的任一操作在一个空的 Key 上执行时就会创建一个新的列表。相似的,如果一个列表操作清空一个列表,那么对应的 key 将被从 key 空间删除。
push 一类的命令的返回值为 list 的长度。这里有一些类表操作和结果的例子:
> rpush mylist A
> rpush mylist B
> lpush mylist first
> lrange mylist 0 -1
操作截图:
注意:lrange 需要两个索引,0 表示 list 开头第一个,-1 表示 list 的倒数第一个,即最后一个。-2 则是 list 的倒数第二个,以此类推。
这些命令都是可变的命令,也就是说你可以一次加入多个元素放入 list:
> rpush mylist 1 2 3 4 5 "foo bar"
> lrange mylist 0 -1
操作截图:
在 Redis 的命令操作中,还有一类重要的操作 pop
,它可以弹出一个元素,简单的理解就是获取并删除第一个元素,和 push
类似的是它也支持双边的操作,可以从右边弹出一个元素也可以从左边弹出一个元素,对应的指令为 rpop
和 lpop
:
> del mylist
> rpush mylist a b c
> rpop mylist
> lrange mylist 0 -1
> lpop mylist
> lrange mylist 0 -1
操作截图:
一个列表最多可以包含 4294967295(2 的 32 次方减一)个元素,这意味着它可以容纳海量的信息,最终瓶颈一般都取决于服务器内存大小。
事实上,在高级的企业架构当中,会把缓存服务器分离开来,因为数据库服务器和缓存服务器的特点各异,比如对于数据库服务器应该用更快、更大的硬盘,而缓存专用服务器则偏向内存性能,一般都是 64GB 起步。
List 阻塞操作
理解阻塞操作对一些请求操作有很大的帮助,关于阻塞操作的作用,这里举一个例子。
假如你要去楼下买一个汉堡,一个汉堡需要花一定的时间才能做出来,非阻塞式的做法是去付完钱走人,过一段时间来看一下汉堡是否做好了,没好就先离开,过一会儿再来,而且要知道可能不止你一个人在买汉堡,在你离开的时候很可能别人会取走你的汉堡,这是很让人烦的事情。
阻塞式就不一样了,付完钱一直在那儿等着,不拿到汉堡不走人,并且后面来的人统统排队。
Redis 提供了阻塞式访问 brpop
和 blpop
命令。用户可以在获取数据不存在时阻塞请求队列,如果在时限内获得数据则立即返回,如果超时还没有数据则返回 nil。
在终端执行:
brpop list 10
brpop mylist 10
List 常见应用场景
分析 List 应用场景需要结合它的特点,List 元素是线性有序的,很容易就可以联想到聊天记录,你一言我一语都有先后,因此 List 很适合用来存储聊天记录等顺序结构的数据。
lpush
在key对应list的头部添加字符串元素
redis 127.0.0.1:6379> lpush mylist "world"
(integer) 1
redis 127.0.0.1:6379> lpush mylist "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>
其中,Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推
rpush
在key对应list的尾部添加字符串元素
redis 127.0.0.1:6379> rpush mylist2 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist2 "world"
(integer) 2
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379>
del
清空集合元素,例如
redis 127.0.0.1:6379> del mylist
linsert
在key对应list的特定位置之前或之后添加字符串元素
redis 127.0.0.1:6379> rpush mylist3 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist3 "world"
(integer) 2
redis 127.0.0.1:6379> linsert mylist3 before "world" "there"
(integer) 3
redis 127.0.0.1:6379> lrange mylist3 0 -1
1) "hello"
2) "there"
3) "world"
redis 127.0.0.1:6379>
lset
设置list中指定下标的元素值(一般用于修改操作)
redis 127.0.0.1:6379> rpush mylist4 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist4 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist4 "three"
(integer) 3
redis 127.0.0.1:6379> lset mylist4 0 "four"
OK
redis 127.0.0.1:6379> lset mylist4 -2 "five"
OK
redis 127.0.0.1:6379> lrange mylist4 0 -1
1) "four"
2) "five"
3) "three"
redis 127.0.0.1:6379>
lrem
从key对应list中删除count个和value相同的元素,count>0时,按从头到尾的顺序删除
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist5 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist5 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist5 2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "foo"
2) "hello"
redis 127.0.0.1:6379>
count<0时,按从尾到头的顺序删除
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist6 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist6 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist6 -2 "hello"
(integer) 2
redis 127.0.0.1:6379> lrange mylist6 0 -1
1) "hello"
2) "foo"
redis 127.0.0.1:6379>
count=0时,删除全部
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 1
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 2
redis 127.0.0.1:6379> rpush mylist7 "foo"
(integer) 3
redis 127.0.0.1:6379> rpush mylist7 "hello"
(integer) 4
redis 127.0.0.1:6379> lrem mylist7 0 "hello"
(integer) 3
redis 127.0.0.1:6379> lrange mylist7 0 -1
1) "foo"
redis 127.0.0.1:6379>
ltrim
保留指定key 的值范围内的数据
redis 127.0.0.1:6379> rpush mylist8 "one"
(integer) 1
redis 127.0.0.1:6379> rpush mylist8 "two"
(integer) 2
redis 127.0.0.1:6379> rpush mylist8 "three"
(integer) 3
redis 127.0.0.1:6379> rpush mylist8 "four"
(integer) 4
redis 127.0.0.1:6379> ltrim mylist8 1 -1
OK
redis 127.0.0.1:6379> lrange mylist8 0 -1
1) "two"
2) "three"
3) "four"
redis 127.0.0.1:6379>
lpop
从list的头部删除元素,并返回删除元素
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> lpop mylist
"hello"
redis 127.0.0.1:6379> lrange mylist 0 -1
1) "world"
redis 127.0.0.1:6379>
rpop
从list的尾部删除元素,并返回删除元素:
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
2) "world"
redis 127.0.0.1:6379> rpop mylist2
"world"
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) "hello"
redis 127.0.0.1:6379>
llen
返回key对应list的长度:
redis 127.0.0.1:6379> llen mylist5
(integer) 2
redis 127.0.0.1:6379>
lindex
返回名称为key的list中index位置的元素:
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) "three"
2) "foo"
redis 127.0.0.1:6379> lindex mylist5 0
"three"
redis 127.0.0.1:6379> lindex mylist5 1
"foo"
redis 127.0.0.1:6379>
rpoplpush
从第一个list的尾部移除元素并添加到第二个list的头部,最后返回被移除的元素值,整个操作是原子的.如果第一个list是空或者不存在返回nil:
rpoplpush lst1 lst1
rpoplpush lst1 lst2
小节面试分析
- 如何基于redis实现一个队列结构?(lpush/rpop)
- 如何基于redis实现一个栈结构?(lpush/lpop)
- 如何基于redis实现一个阻塞式队列?(lpush/brpop)
- 如何实现秒杀活动的公平性?(先进先出-FIFO)
- 通过list结构实现一个消息队列(顺序)吗?(可以,FIFO->lpush,rpop)
- 用户注册时的邮件发送功能如何提高其效率?(邮件发送是要调用三方服务,底层通过队列优化其效率,队列一般是list结构)
- 如何动态更新商品的销量列表?(卖的好的排名靠前一些,linsert)
- 商家的粉丝列表使用什么结构实现呢?(list结构)
Set类型应用实践
Redis的Set类似Java中的HashSet,是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
Redis 无序集合
Redis 集合(Set)是一个无序的字符串集合。你可以以 O(1) 的时间复杂度(无论集合中有多少元素时间复杂度都是常量)完成添加、删除以及测试元素是否存在。
Redis 集合拥有令人满意的不允许包含相同成员的属性,多次添加相同的元素,最终在集合里只会有一个元素,这意味着它可以非常方便地对数据进行去重操作。一个 Redis 集合的非常有趣的事情是它支持一些服务端的命令从现有的集合出发去进行集合运算,因此你可以在非常短的时间内进行合并(unions),求交集(intersections),找出不同的元素(differences of sets)。
> sadd myset 1 2 3
> smembers myset
sadd 命令产生一个无序集合,返回集合的元素个数。smembers 用于查看集合。
操作截图:
sismember 用于查看集合是否存在,匹配项包括集合名和元素(用于查看该元素是否是集合的成员)。匹配成功返回 1,匹配失败返回 0。
> sismember myset 3
> sismember myset 30
> sismember mys 3
操作截图:
Redis 有序集合
Redis 有序集合与普通集合非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每一个成员都关联了一个权值,这个权值被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是权值可以是重复的。
使用有序集合你可以以非常快的速度 O(log(N))
添加、删除和更新元素。因为元素是有序的,所以你也可以很快的根据权值(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。在有序集合中,你可以很快捷的访问一切你需要的东西:有序的元素,快速的存在性测试,快速访问集合的中间元素!简而言之使用有序集合你可以完成许多对性能有极端要求的任务,而那些任务使用其它类型的数据库真的是很难完成的。
zadd 与 sadd 类似,但是在元素之前多了一个参数,这个参数便是用于排序的。形成一个有序的集合。
> zadd hackers 1940 "Alan Kay"
> zadd hackers 1957 "Sophie Wilson"
> zadd hackers 1953 "Richard Stallman"
> zadd hackers 1949 "Anita Borg"
> zadd hackers 1965 "Yukihiro Matsumoto"
> zadd hackers 1914 "Hedy Lamarr"
> zadd hackers 1916 "Claude Shannon"
> zadd hackers 1969 "Linus Torvalds"
> zadd hackers 1912 "Alan Turing"
查看集合:zrange 是查看正序的集合,zrevrange 是查看反序的集合。0 表示集合第一个元素,-1 表示集合的倒数第一个元素。
> zrange hackers 0 -1
> zrevrange hackers 0 -1
操作截图:
使用 withscores 参数返回记录值。
> zrange hackers 0 -1 withscores
操作截图:
sadd
添加元素,重复元素添加失败,返回0
127.0.0.1:6379> sadd name tony
(integer) 1
127.0.0.1:6379> sadd name hellen
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 0
smembers
获取集合中成员,例如
127.0.0.1:6379> smembers name
- “hellen”
- “rose”
- “tony”
spop
移除并返回集合中的一个随机元素
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> spop internet
"rabbitmq"
127.0.0.1:6379> spop internet
"nginx"
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
scard
获取集合中的成员个数
127.0.0.1:6379> scard name
(integer) 3
smove
移动一个元素到另外一个集合
127.0.0.1:6379> sadd internet amoeba nginx redis
(integer) 3
127.0.0.1:6379> sadd bigdata hadopp spark rabbitmq
(integer) 3
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
3) "rabbitmq"
127.0.0.1:6379> smove bigdata internet rabbitmq
(integer) 1
127.0.0.1:6379> smembers internet
1) "amoeba"
2) "redis"
3) "rabbitmq"
4) "nginx"
127.0.0.1:6379> smembers bigdata
1) "hadopp"
2) "spark"
127.0.0.1:6379>
sunion
实现集合的并集操作
127.0.0.1:6379> sunion internet bigdata
1) "redis"
2) "nginx"
3) "rabbitmq"
4) "amoeba"
5) "hadopp"
6) "spark"
小节面试分析
- 朋友圈的点赞功能你如何实现?(sadd,srem,smembers,scard)
- 如何实现一个网站投票统计程序?
- 你知道微博中的关注如何实现吗?
总结(Summary)
常见问题分析
- Redis是什么?(分布式Key/Value结构的缓存数据库,非关系型数据,NoSql数据库)
- Redis数据库诞生的背景?(关系型数据库的访问压力比较大,本地内存不支持多服务实例共享)
- Redis数据库的基本架构?(C/S,redis-cli,redis-server)
- 你了解Redis有哪些基础指令?(redis-cli,redis-server,exit,clear,type,expire,shutdown,help,?,keys,flushall,flushdb)
- 字符串类型有什么特点?(所有值都是字符串,空间动态分配,可以实现整数值的递增,递减,实现日志记录)
- 操作字符串类型(string)的常用指令?(set,get,strlen,append,mset,mget,incr,incrby,decr,decrby)
- 哈希类型(hash)数据有什么特性?(就是值还可以使用key/value结构存储,key无序,key相同值覆盖,存储对象方便)
- 操作哈希类型(hash)的常用指令?(hset,hget,hgetall,hexits,hdel,hkeys,hvals,hincrby,hmget)
- 列表类型(list)数据有什么特性?(链表,会记录添加的元素的顺序,元素允许重复,可以实现FIFO,FILO这些特性)
- 操作列表类型(list)类型的常用指令?(lpush,rpop,rpush,lpop,lrem,lindex,ltrim,lset,linsert,lrange,rpoplpush,lpos)
- Set类型数据的特性?(散列,不记录元素添加顺序,不允许元素重复)
- 操作set类型的常用指令?(sadd,smembers,spop,smove,scard,sunion)
- Redis中各种数据类型的应用场景?
Redis分布式缓存技术应用
1)一种性能优化策略?(从内存加载数据)
2)两种缓存(内存)应用套路?(本地缓存和分布式缓存)
3)Redis中的5种数据类型(string,hash,list,set,zset,…)
4)两套Java客户端API?(Jedis,RedisTemplate)
5)3种数据可靠性方案?(持久化,事务,主从架构,哨兵,集群)
6)结合菜单模块实现两种综合性缓存应用解决方案?(RedisTemplate+redis,Aop+redis)
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Sep 22,2022