简介
Redis,Remote Dictionary Server,远程词典服务器,一个基于内存的键值型NoSQL数据库。
特点:
- 键值(key-value)型,value支持多种数据结构。
- 单线程,每个命令具备原子性。
虽然从6版本开始,已经支持多线程,但是这里的多线程仅仅针对网络请求,在核心的命令执行,依旧是单线程。 - 低延迟,速度快。
原因:基于内存、IO多路复用、良好的编码,其中最主要的原因是基于内存。 - 支持数据持久化。
Redis会定期将数据从内存持久化到磁盘 - 支持主从集群、分片集群。
安装
本文讨论的是在Linux上安装Redis,一般也都是在Linux服务器上安装Redis,而且Redis官方没有提供Windows的安装包,我们在网上看到的一些Windows的安装包,并不是Redis官方的。
gcc依赖
Redis是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖。
1 | yum install -y gcc tcl |
解压安装包
可以通过官网下载安装包,然后上传至Linux服务器。或者直接在Linux服务器上通过wget的方式下载。
1 | wget https://github.com/redis/redis/archive/7.2.3.tar.gz |
下载完成后,可以把redis压缩包,移动到一个合适的目录(例如/usr/local/src
),redis会被安装在我们解压目录的bin中。
解压缩:
1 | tar -xzf redis-7.2.3.tar.gz |
解压后,进入redis目录:
1 | cd redis-7.2.3/ |
编译并安装:
1 | make && make install |
此时,我们会发现在解压包下,多了一个目录bin
,redis就被安装在该目录下。
此外,redis还会在/usr/local/bin/
目录下新建"快捷方式"。因为,/usr/local/bin/
目录已经默认配置到环境变量,所以可以在任意目录下运行这些命令。
redis-cli
:是redis提供的命令行客户端。redis-server
:是redis的服务端启动脚本。redis-sentinel
:是redis的哨兵启动脚本.
启动
Redis的启动方式有很多种,例如:
- 默认启动
- 指定配置启动
- 系统服务启动
默认启动
安装完成后,在任意目录输入redis-server
命令即可启动Redis。
示例代码:
1 | redis-server |
运行结果:
1 | 24186:C 28 Nov 2023 22:20:06.412 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo |
这种启动属于前台启动
,会阻塞整个会话窗口,窗口关闭或者按下CTRL + C
则Redis停止。不推荐使用。
指定配置启动
如果要让Redis以"后台"方式启动,可以修改Redis配置文件。
配置文件
配置文件路径:/usr/local/src/redis-7.2.3/redis.conf
。
启动Redis
指定配置文件,启动Redis。
1 | redis-server /usr/local/src/redis-7.2.3/redis.conf |
启动之后,我们敲入redis-cli
命令,然后ping
一下:
1 | redis-cli |
因为我们上文设置了密码,所以需要先认证:
1 | redis-cli |
系统服务启动
新建一个系统服务文件:
1 | vim /etc/systemd/system/redis.service |
内容如下:
1 | [Unit] |
重载系统服务:
1 | systemctl daemon-reload |
这样,便加入了系统服务。
- 启动:
systemctl start redis
。 - 停止:
systemctl stop redis
。 - 重启:
systemctl restart redis
。 - 查看状态:
systemctl status redis
。 - 开机自启:
systemctl enable redis
。
命令行客户端
Redis安装完成后就自带了命令行客户端,redis-cli
。
使用方式如下:
1 | redis-cli [options] [commonds] |
常见的options
有:
-h 127.0.0.1
:指定要连接的redis节点的IP地址,默认是127.0.0.1-p 6379
:指定要连接的redis节点的端口,默认是6379-a 123321
:指定redis的访问密码
commonds
举例:
- 选择0号库:
select 0
- 关闭Redis服务端:
shutdown
注意,该命令是关闭Redis服务端。
图形化客户端
图形化客户端有很多,例如:
- Redis官方的
RedisInsight
下载地址:https://redis.com/redis-enterprise/redis-insight/ - DataGrip
问题解决
卸载
- 停止redis服务器
- 删除
/usr/local/bin
1
2
3cd /usr/local/bin
ll redis*
rm -f redis* - 删除解压后的文件目录和所有文件
1
2cd /usr/local/src/
rm -rf redis-7.0.7 - 如果我们配置了加入系统服务,需要把相关的文件(
/etc/systemd/system/redis.service
)也删掉。 - 如果我们还配置了开机启动,还需要先关闭开机启动。
Key的层级
Redis没有类似MySQL中的Table的概念,我们该如何区分不同类型的key呢?
例如,需要存储用户、商品信息到redis,有一个用户id是1,有一个商品id恰好也是1
Redis的key允许有多个单词形成层级结构,多个单词之间用:
隔开,格式如下:
1 | 项目名:业务名:类型:id |
例如我们的项目名称叫kaka
,有user
和product
两种不同类型的数据,我们可以这样定义key:
- user相关的key:
kaka:user:1
- product相关的key:
kaka:product:1
这个格式并非固定,也可以根据自己的需求来删除或添加词条。
数据结构
分类
Redis的数据结构有很多种,常见的是8种。
5种基本类型:
String
1
hello world
Hash
1
{name: "Jack", age: 21}
List
1
[A -> B -> C -> C]
Set
1
{A, B, C}
SortedSet
1
{A: 1, B: 2, C: 3}
3种特殊类型:
GEO
1
{A:(120.3, 30.5)}
BitMap
1
0110110101110101011
HyperLog
1
0110110101110101011
除了上述8种,还有很多,例如消息队列等。
帮助文档
在Redis的官网可以查看不同数据结构的操作命令
也可以通过命令查看帮助。
help
命令,示例代码:
1 | help |
运行结果:
1 | redis-cli 7.2.3 |
查看keys
命令的帮助,示例代码:
1 | help keys |
运行结果:
1 | KEYS pattern |
通用命令
String类型
String类型,也就是字符串类型,其value是字符串。
根据字符串的格式不同,又可以分为3类:
string
:普通字符串。int
:整数类型,可以做自增、自减操作。float
:浮点类型,可以做自增、自减操作。
String类型的常见命令:
SET
,添加或者修改已经存在的一个String类型的键值对。GET
,根据key获取String类型的value。MSET
,批量添加多个String类型的键值对。MGET
,根据多个key获取多个String类型的value。INCR
,让一个整型的key自增1。DECR
,让一个整型的key自减1。INCRBY
,让一个整型的key自增并指定步长。例如:incrby num 2
,让num值自增2。INCRBYFLOAT
,让一个浮点类型的数字自增并指定步长。SETNX
,添加一个String类型的键值对,前提是这个key不存在,否则不执行。SETEX
,添加一个String类型的键值对,并且指定有效期。
在SET命令中,有许多可用选项可以修改命令的行为。
1 | SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX] |
EX seconds
,设置指定的终止时间,以秒为单位。PX milliseconds
,设置指定的终止时间,以毫秒为单位。NX
,仅在 不存在 的情况下设置key。XX
,仅在 已存在 的情况下设置key。
例如,仅在不存在情况下,设置键coo
,有效期为60秒:
1 | SET coo redis EX 60 NX |
如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:
KEY | VALUE |
---|---|
kaka:user:1 | {“id”:1, “name”: “Jack”, “age”: 21} |
kaka:product:1 | {“id”:1, “name”: “小米11”, “price”: 4999} |
示例代码:
1 | set kaka:user:1 '{"id":1, "name": "Jack", "age": 21}' |
但是这个方法并不好,下文的Hash类型
是一种更好的方法。
Hash类型
Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构。
String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便。
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD。value,分为了两部分,一部分是field,一部分是value
Hash的常见命令有:
HSET key field value
,添加或者修改hash类型key的field的值。HGET key field
,获取一个hash类型key的field的值。HMSET
,批量添加多个hash类型key的field的值。HMGET
,批量获取多个hash类型key的field的值。HGETALL
,获取一个hash类型的key中的所有的field和value。HKEYS
,获取一个hash类型的key中的所有的field。HVALS
,获取一个hash类型的key中的所有的value。HINCRBY
,让一个hash类型key的字段值自增并指定步长。HSETNX
,添加一个hash类型的key的field值,前提是这个field不存在,否则不执行。
可以一次执行多个field的值,示例代码:
1 | HSET kaka:user:1 name Lee age 23 sex male |
List类型
Redis中的List类型与Java中的LinkedList类似,可以看作是一个双向链表结构,既可以支持正向检索和也可以支持反向检索。
特征也与LinkedList类似:
- 有序
- 元素可以重复
- 插入和删除快
- 查询速度一般
List的常见命令有:
LPUSH key element ...
,向列表左侧插入一个或多个元素。LPOP key
,移除并返回列表左侧的第一个元素,没有则返回nil
。RPUSH key element ...
,向列表右侧插入一个或多个元素。RPOP key
,移除并返回列表右侧的第一个元素。LRANGE key star end
,返回一段角标范围内的所有元素。BLPOP
和BRPOP
,与LPOP
和RPOP
类似,只不过在没有元素时等待指定时间,而不是直接返回nil
。
使用方法:
- 如何利用List结构模拟一个栈?
入口和出口在同一边 - 如何利用List结构模拟一个队列?
入口和出口在不同边 - 如何利用List结构模拟一个阻塞队列?
入口和出口在不同边,出队时采用BLPOP或BRPOP
Set类型
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。
其特点有:
- 无序
- 元素不可重复
- 查找快
- 支持交集、并集、差集等功能
Set的常见命令有:
SADD key member ...
,向set中添加一个或多个元素。SREM key member ...
,移除set中的指定元素。SCARD key
,返回set中元素的个数。SISMEMBER key member
,判断一个元素是否存在于set中。SMEMBERS
,获取set中的所有元素。SINTER key1 key2 ...
,求key1与key2的交集。
SortedSet类型
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。
SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加hash表。
SortedSet具备下列特性:
- 可排序
- 元素不重复
- 查询速度快
SortedSet的常见命令有:
ZADD key score member
,添加一个或多个元素到SortedSet,如果已经存在则更新其score值。ZREM key member
,删除SortedSet中的一个指定元素。ZSCORE key member
,获取SortedSet中的指定元素的score值。ZRANK key member
,获取SortedSet中的指定元素的排名。ZCARD key
,获取SortedSet中的元素个数。ZCOUNT key min max
,统计score值在给定范围内的所有元素的个数ZINCRBY key increment member
,让SortedSet中的指定元素自增,步长为指定的increment值。ZRANGE key min max
,按照score排序后,获取指定排名范围内的元素。ZRANGEBYSCORE key min max
,按照score排序后,获取指定score范围内的元素。ZDIFF、ZINTER、ZUNION
,求差集、交集、并集。
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:
- 升序获取SortedSet中的指定元素的排名:
ZRANK key member
- 降序获取SortedSet中的指定元素的排名:
ZREVRANK key memeber
再举一个例子,将班级的下列学生得分存入Redis的SortedSet中:
1 | Jack 85, Lucy 89, Rose 82, Tom 95, Jerry 78, Amy 92, Miles 76 |
示例代码:
1 | ZADD stus 85 Jack 89 Lucy 82 Rose 95 Tom 78 Jerry 92 Amy 76 Miles |
Java客户端
四种客户端
- Jedis
以Redis命令作为方法名称,学习成本低,简单实用。但是Jedis实例是线程不安全的,多线程环境下需要基于连接池来使用。 - Lettuce
Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式。 - Redisson
Redisson是一个基于Redis实现的分布式、可伸缩的Java数据结构集合。包含了诸如Map、Queue、Lock、 Semaphore、AtomicLong等强大功能。 - SpringDataRedis
提供了对不同Redis客户端的整合(Lettuce和Jedis)
在本文,我们讨论Jedis
和SpringDataRedis
。
Jedis
Github地址:https://github.com/redis/jedis
通过这个Github地址,我们可以看出来,jedis是redis官方的。
快速入门
示例代码:
1 | package com.kakawanyifan; |
运行结果:
1 | result = OK |
连接池
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,推荐使用Jedis连接池代替Jedis的直连方式。
Jedis连接池,JedisConnectionFactory
:
1 | package com.heima.jedis.util; |
建立连接,示例代码:
1 | // 建立连接 |
释放连接,示例代码:
1 | jedis.close(); |
特别的,我们可以点进jedis.close()
方法,会发现该方法内部根据是否有连接池判断是应该归还连接,还是释放连接。
示例代码:
1 | public void close() { |
SpringDataRedis
一个现象
关于SpringDataRedis,我们在《基于Java的后端开发入门:21.SpringBoot [1/3]》的"整合Redis客户端"部分,已经讨论过了。
在这里做一些补充,先看一个现象。
示例代码:
1 | package com.kakawanyifan.demo; |
运行结果:
1 | Kaka |
这个没有任何问题,然后我们通过redis-cli
命令get name
,示例代码:
1 | get name |
运行结果:
1 | (nil) |
没有?
通过keys
看一下,示例代码:
1 | keys *name* |
运行结果:
1 | 1) "\xac\xed\x00\x05t\x00\x04name" |
key怎么这么奇怪?
自定义序列化
RedisTemplate默认是采用JDK序列化,这种方式存在两个问题:
- 可读性差
- 内存占用较大
上文的"\xac\xed\x00\x05t\x00\x04name"
就是例子。
我们可以自定义RedisTemplate的序列化方式:
1 | package com.kakawanyifan.demo; |
- 对于value,采用了JSON序列化来代替默认的JDK序列化方式。
对象序列化
现在,假设我们有一个对象User
,我们要把这个对象保存到Redis中。
User
类,示例代码:
1 | package com.kakawanyifan.demo; |
存取对象,示例代码:
1 | package com.kakawanyifan.demo; |
运行结果:
1 | User(id=1, user=kaka) |
我们通过get
命令看一下,示例代码:
1 | get user:1 |
运行结果:
1 | "{\"@class\":\"com.kakawanyifan.demo.User\",\"id\":1,\"user\":\"kaka\"}" |
在Redis的内容中,有这么一项@class
,专门标明了User对象的类com.kakawanyifan.demo.User
(序列化时对应的class名称)。
StringRedisTemplate
在上文,专门标明了序列化时对应的class名称,这样会带来额外的内存开销,为了节省内存空间,可以不使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。
特别的,SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。
省去了我们自定义RedisTemplate的序列化方式的步骤。
示例代码:
1 | package com.kakawanyifan.demo; |
运行结果:
1 | User(id=2, user=kaka-2) |
通过get
命令看一下,示例代码:
1 | get user:2 |
运行结果:
1 | "{\"id\":2,\"user\":\"kaka-2\"}" |
Python客户端
redis-py
安装命令:
1 | pip install redis |
PiPy地址:https://pypi.org/project/redis/
GitHub地址:https://github.com/redis/redis-py
官方文档:https://redis.readthedocs.io/en/stable/index.html
decode_responses
示例代码:
1 | import redis |
运行结果:
1 | b'Kaka' |
默认是字节类型,可以设置decode_responses=True
,示例代码:
1 | import redis |
运行结果:
1 | Kaka |
连接池
redis-py
使用ConnectionPool
来管理对一个RedisServer的所有连接,避免每次建立、释放连接的开销。
示例代码:
1 | import redis |
运行结果:
1 | Kaka |
常见操作
关于Redis中的各种操作,可以参考官方文档。
https://redis.readthedocs.io/en/stable/commands.html
这里列举一些常用的。
String
-
set
1
set(name, value, ex=None, px=None, nx=False, xx=False)
参数:
ex
,过期时间(秒)。px
,过期时间(毫秒)。nx
,如果设置为True,则只有name不存在时,当前set操作才执行。xx
,如果设置为True,则只有name存在时,当前set操作才执行。
-
get
,获取值。 -
mget
,批量获取值。 -
getset(name, value)
,设置新值并获取原来的值。 -
strlen(name)
,返回name对应值的字节长度(默认字符集是UTF-8,一个汉字三个字节) -
incr(name, amount=1)
,自增name对应的值,当name不存在时,则创建name=amount,否则,则自增。
incrbyfloat
方法自增浮点数类型。
decr
进行自减操作。 -
append(key, value)
,在name对应的值后面追加内容。
hash
hset(name, key, value)
,hmset(name, mapping)
,在name对应的hash中批量设置键值对:1
r.hmset("hash1", {"k1": "v1", "k2": "v2", "k3": "v3"})
hkeys(name)
,获取所有的keyhvals(name)
,得到所有的value。hgetall(name)
,取出所有的键值对。hdel(name)
,删除。hincrby(name, key, amount=1)
,自增自减整数。hincrbyfloat(name, key, amount=1.0)
表示自增自减浮点数。
list
lpush(name,values)
,增加。rpush
表示从右边增加。r.lset(name, index, value)
,对name对应的list中的某一个索引位置重新赋值。r.lrem(name, value, num)
,删除。1
2
3
4
5
6# 将列表中左边第一次出现的"11"删除
r.lrem("list2", "11", 1)
# 将列表中右边第一次出现的"99"删除
r.lrem("list2", "99", -1)
# 将列表中所有的"22"删除
r.lrem("list2", "22", 0)lindex(name, index)
,取值。
set
sadd(name, values)
,新增。srem(name, values)
,在name对应的集合中删除某些值。
其他操作
delete(name)
,删除。expire(name ,time)
,为某个redis的某个name设置超时时间。rename(src, dst)
,重命名。
管道
Redis默认在执行每次请求都会创建连接(连接池申请连接)和断开连接(归还连接池),如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pipline是原子性操作。
示例代码:
1 | import redis |